diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2013-01-18 16:46:04 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2013-01-18 16:46:04 +0100 |
commit | 63601400e476c6cf43d985f3e7b9864681695ed4 (patch) | |
tree | f7846203a952e38aaf66989d0a4702779f549962 /tests | |
parent | 8ff01378c9e0207f9169b81966a51def645b6a51 (diff) |
Update to MediaWiki 1.20.2
this update includes:
* adjusted Arch Linux skin
* updated FluxBBAuthPlugin
* patch for https://bugzilla.wikimedia.org/show_bug.cgi?id=44024
Diffstat (limited to 'tests')
168 files changed, 13978 insertions, 2511 deletions
diff --git a/tests/RunSeleniumTests.php b/tests/RunSeleniumTests.php index 9cadd759..28501eaa 100644 --- a/tests/RunSeleniumTests.php +++ b/tests/RunSeleniumTests.php @@ -24,12 +24,12 @@ * http://www.gnu.org/copyleft/gpl.html */ -$IP = dirname( dirname( __FILE__ ) ); +$IP = dirname( __DIR__ ); define( 'SELENIUMTEST', true ); -//require_once( dirname( __FILE__ ) . '/../maintenance/commandLine.inc' ); -require( dirname( __FILE__ ) . '/../maintenance/Maintenance.php' ); +//require_once( __DIR__ . '/../maintenance/commandLine.inc' ); +require( __DIR__ . '/../maintenance/Maintenance.php' ); require_once( 'PHPUnit/Runner/Version.php' ); if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '>=' ) ) { @@ -43,7 +43,7 @@ if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '>=' ) ) { require_once( 'PHPUnit/Extensions/SeleniumTestCase.php' ); include_once( 'PHPUnit/Util/Log/JUnit.php' ); -require_once( dirname( __FILE__ ) . "/selenium/SeleniumServerManager.php" ); +require_once( __DIR__ . "/selenium/SeleniumServerManager.php" ); class SeleniumTester extends Maintenance { protected $selenium; diff --git a/tests/TestsAutoLoader.php b/tests/TestsAutoLoader.php index 41bddfcd..42f5f68a 100644 --- a/tests/TestsAutoLoader.php +++ b/tests/TestsAutoLoader.php @@ -1,7 +1,7 @@ <?php global $wgAutoloadClasses; -$testFolder = dirname( __FILE__ ); +$testFolder = __DIR__; $wgAutoloadClasses += array( @@ -9,6 +9,7 @@ $wgAutoloadClasses += array( 'MediaWikiTestCase' => "$testFolder/phpunit/MediaWikiTestCase.php", 'MediaWikiPHPUnitCommand' => "$testFolder/phpunit/MediaWikiPHPUnitCommand.php", 'MediaWikiLangTestCase' => "$testFolder/phpunit/MediaWikiLangTestCase.php", + 'NewParserTest' => "$testFolder/phpunit/includes/parser/NewParserTest.php", //includes 'BlockTest' => "$testFolder/phpunit/includes/BlockTest.php", @@ -16,7 +17,7 @@ $wgAutoloadClasses += array( //API 'ApiFormatTestBase' => "$testFolder/phpunit/includes/api/format/ApiFormatTestBase.php", 'ApiTestCase' => "$testFolder/phpunit/includes/api/ApiTestCase.php", - 'ApiTestUser' => "$testFolder/phpunit/includes/api/ApiTestUser.php", + 'TestUser' => "$testFolder/phpunit/includes/TestUser.php", 'MockApi' => "$testFolder/phpunit/includes/api/ApiTestCase.php", 'RandomImageGenerator' => "$testFolder/phpunit/includes/api/RandomImageGenerator.php", 'UserWrapper' => "$testFolder/phpunit/includes/api/ApiTestCase.php", @@ -24,6 +25,10 @@ $wgAutoloadClasses += array( //Selenium 'SeleniumTestConstants' => "$testFolder/selenium/SeleniumTestConstants.php", + //maintenance + 'DumpTestCase' => "$testFolder/phpunit/maintenance/DumpTestCase.php", + 'BackupDumper' => "$testFolder/../maintenance/backup.inc", + //Generic providers 'MediaWikiProvide' => "$testFolder/phpunit/includes/Providers.php", ); diff --git a/tests/jasmine/SpecRunner.html b/tests/jasmine/SpecRunner.html index 6af9b0c3..63d0fdfa 100644 --- a/tests/jasmine/SpecRunner.html +++ b/tests/jasmine/SpecRunner.html @@ -1,42 +1,28 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" - "http://www.w3.org/TR/html4/loose.dtd"> -<html> -<head> - <title>Jasmine Test Runner</title> - <link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.1/jasmine.css"> - <script type="text/javascript" src="lib/jasmine-1.0.1/jasmine.js"></script> - <script type="text/javascript" src="lib/jasmine-1.0.1/jasmine-html.js"></script> +<!DOCTYPE html> +<html lang="en" dir="ltr"> + <head> + <title>Jasmine Test Runner</title> + <meta charset="UTF-8" /> + <link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.1/jasmine.css"> + <script src="lib/jasmine-1.0.1/jasmine.js"></script> + <script src="lib/jasmine-1.0.1/jasmine-html.js"></script> - <!-- include source files here... --> - <script type="text/javascript" src="../../load.php?debug=true&lang=en&modules=jquery%7Cmediawiki&only=scripts&skin=vector"></script> - - <script type="text/javascript" src="../../resources/mediawiki/mediawiki.js"></script> + <!-- include source files here... --> + <script src="../../load.php?debug=true&lang=en&modules=startup&only=scripts&skin=vector&*"></script> + <script> + mw.loader.load( ['mediawiki.jqueryMsg'] ); + </script> - <script type="text/javascript" src="../../resources/mediawiki.language/mediawiki.language.js"></script> - <script type="text/javascript" src="../../resources/mediawiki/mediawiki.jqueryMsg.js"></script> - <script type="text/javascript" src="../../resources/mediawiki/mediawiki.Uri.js"></script> -<!-- - <script type="text/javascript" src="../../resources/mediawiki/mediawiki.api.js"></script> - <script type="text/javascript" src="../../resources/mediawiki/mediawiki.api.edit.js"></script> ---> + <!-- insert test data files here --> + <script src="spec/mediawiki.jqueryMsg.spec.data.js"></script> - <!-- insert test data files here --> - <script type="text/javascript" src="spec/mediawiki.jqueryMsg.spec.data.js"></script> - - <!-- include spec files here... --> - <script type="text/javascript" src="spec/mediawiki.Uri.spec.js"></script> - <!-- - <script type="text/javascript" src="spec/mw.Api.spec.js"></script> - <script type="text/javascript" src="spec/mw.Api.edit.spec.js"></script> - --> - <script type="text/javascript" src="spec/mediawiki.jqueryMsg.spec.js"></script> - -</head> + <!-- include spec files here... --> + <script src="spec/mediawiki.jqueryMsg.spec.js"></script> + </head> <body> -<script type="text/javascript"> - jasmine.getEnv().addReporter( new jasmine.TrivialReporter() ); - jasmine.getEnv().execute(); -</script> - + <script> + jasmine.getEnv().addReporter( new jasmine.TrivialReporter() ); + jasmine.getEnv().execute(); + </script> </body> </html> diff --git a/tests/jasmine/spec/mediawiki.Uri.spec.js b/tests/jasmine/spec/mediawiki.Uri.spec.js deleted file mode 100644 index 721ccb38..00000000 --- a/tests/jasmine/spec/mediawiki.Uri.spec.js +++ /dev/null @@ -1,307 +0,0 @@ -( function() { - - // ensure we have a generic URI parser if not running in a browser - if ( !mw.Uri ) { - mw.Uri = mw.UriRelative( 'http://example.com/' ); - } - - describe( "mw.Uri", function() { - - describe( "should work well in loose and strict mode", function() { - - function basicTests( strict ) { - - describe( "should parse a simple HTTP URI correctly", function() { - - var uriString = 'http://www.ietf.org/rfc/rfc2396.txt'; - var uri; - if ( strict ) { - uri = new mw.Uri( uriString, strict ); - } else { - uri = new mw.Uri( uriString ); - } - - it( "should have basic object properties", function() { - expect( uri.protocol ).toEqual( 'http' ); - expect( uri.host ).toEqual( 'www.ietf.org' ); - expect( uri.port ).not.toBeDefined(); - expect( uri.path ).toEqual( '/rfc/rfc2396.txt' ); - expect( uri.query ).toEqual( {} ); - expect( uri.fragment ).not.toBeDefined(); - } ); - - describe( "should construct composite components of URI on request", function() { - it( "should have empty userinfo", function() { - expect( uri.getUserInfo() ).toEqual( '' ); - } ); - - it( "should have authority equal to host", function() { - expect( uri.getAuthority() ).toEqual( 'www.ietf.org' ); - } ); - - it( "should have hostport equal to host", function() { - expect( uri.getHostPort() ).toEqual( 'www.ietf.org' ); - } ); - - it( "should have empty string as query string", function() { - expect( uri.getQueryString() ).toEqual( '' ); - } ); - - it( "should have path as relative path", function() { - expect( uri.getRelativePath() ).toEqual( '/rfc/rfc2396.txt' ); - } ); - - it( "should return a uri string equivalent to original", function() { - expect( uri.toString() ).toEqual( uriString ); - } ); - } ); - } ); - } - - describe( "should work in loose mode", function() { - basicTests( false ); - } ); - - describe( "should work in strict mode", function() { - basicTests( true ); - } ); - - } ); - - it( "should parse a simple ftp URI correctly with user and password", function() { - var uri = new mw.Uri( 'ftp://usr:pwd@192.0.2.16/' ); - expect( uri.protocol ).toEqual( 'ftp' ); - expect( uri.user ).toEqual( 'usr' ); - expect( uri.password ).toEqual( 'pwd' ); - expect( uri.host ).toEqual( '192.0.2.16' ); - expect( uri.port ).not.toBeDefined(); - expect( uri.path ).toEqual( '/' ); - expect( uri.query ).toEqual( {} ); - expect( uri.fragment ).not.toBeDefined(); - } ); - - it( "should parse a simple querystring", function() { - var uri = new mw.Uri( 'http://www.google.com/?q=uri' ); - expect( uri.protocol ).toEqual( 'http' ); - expect( uri.host ).toEqual( 'www.google.com' ); - expect( uri.port ).not.toBeDefined(); - expect( uri.path ).toEqual( '/' ); - expect( uri.query ).toBeDefined(); - expect( uri.query ).toEqual( { q: 'uri' } ); - expect( uri.fragment ).not.toBeDefined(); - expect( uri.getQueryString() ).toEqual( 'q=uri' ); - } ); - - describe( "should handle multiple value query args (overrideKeys on)", function() { - var uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', { overrideKeys: true } ); - it ( "should parse with multiple values", function() { - expect( uri.query.m ).toEqual( 'bar' ); - expect( uri.query.n ).toEqual( '1' ); - } ); - it ( "should accept multiple values", function() { - uri.query.n = [ "x", "y", "z" ]; - expect( uri.toString() ).toContain( 'm=bar' ); - expect( uri.toString() ).toContain( 'n=x&n=y&n=z' ); - expect( uri.toString().length ).toEqual( 'http://www.example.com/dir/?m=bar&n=x&n=y&n=z'.length ); - } ); - } ); - - describe( "should handle multiple value query args (overrideKeys off)", function() { - var uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', { overrideKeys: false } ); - it ( "should parse with multiple values", function() { - expect( uri.query.m.length ).toEqual( 2 ); - expect( uri.query.m[0] ).toEqual( 'foo' ); - expect( uri.query.m[1] ).toEqual( 'bar' ); - expect( uri.query.n ).toEqual( '1' ); - } ); - it ( "should accept multiple values", function() { - uri.query.n = [ "x", "y", "z" ]; - expect( uri.toString() ).toContain( 'm=foo&m=bar' ); - expect( uri.toString() ).toContain( 'n=x&n=y&n=z' ); - expect( uri.toString().length ).toEqual( 'http://www.example.com/dir/?m=foo&m=bar&n=x&n=y&n=z'.length ); - } ); - it ( "should be okay with removing values", function() { - uri.query.m.splice( 0, 1 ); - delete uri.query.n; - expect( uri.toString() ).toEqual( 'http://www.example.com/dir/?m=bar' ); - uri.query.m.splice( 0, 1 ); - expect( uri.toString() ).toEqual( 'http://www.example.com/dir/' ); - } ); - } ); - - describe( "should deal with an all-dressed URI with everything", function() { - var uri = new mw.Uri( 'http://auth@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=value+%28escaped%29#top' ); - - it( "should have basic object properties", function() { - expect( uri.protocol ).toEqual( 'http' ); - expect( uri.user ).toEqual( 'auth' ); - expect( uri.password ).not.toBeDefined(); - expect( uri.host ).toEqual( 'www.example.com' ); - expect( uri.port ).toEqual( '81' ); - expect( uri.path ).toEqual( '/dir/dir.2/index.htm' ); - expect( uri.query ).toEqual( { q1: '0', test1: null, test2: 'value (escaped)' } ); - expect( uri.fragment ).toEqual( 'top' ); - } ); - - describe( "should construct composite components of URI on request", function() { - it( "should have userinfo", function() { - expect( uri.getUserInfo() ).toEqual( 'auth' ); - } ); - - it( "should have authority equal to auth@hostport", function() { - expect( uri.getAuthority() ).toEqual( 'auth@www.example.com:81' ); - } ); - - it( "should have hostport equal to host:port", function() { - expect( uri.getHostPort() ).toEqual( 'www.example.com:81' ); - } ); - - it( "should have query string which contains all components", function() { - var queryString = uri.getQueryString(); - expect( queryString ).toContain( 'q1=0' ); - expect( queryString ).toContain( 'test1' ); - expect( queryString ).not.toContain( 'test1=' ); - expect( queryString ).toContain( 'test2=value+%28escaped%29' ); - } ); - - it( "should have path as relative path", function() { - expect( uri.getRelativePath() ).toContain( uri.path ); - expect( uri.getRelativePath() ).toContain( uri.getQueryString() ); - expect( uri.getRelativePath() ).toContain( uri.fragment ); - } ); - - } ); - } ); - - describe( "should be able to clone itself", function() { - var original = new mw.Uri( 'http://en.wiki.local/w/api.php?action=query&foo=bar' ); - var clone = original.clone(); - - it( "should make clones equivalent", function() { - expect( original ).toEqual( clone ); - expect( original.toString() ).toEqual( clone.toString() ); - } ); - - it( "should be able to manipulate clones independently", function() { - // but they are still different objects - expect( original ).not.toBe( clone ); - // and can diverge - clone.host = 'fr.wiki.local'; - expect( original.host ).not.toEqual( clone.host ); - expect( original.toString() ).not.toEqual( clone.toString() ); - } ); - } ); - - describe( "should be able to construct URL from object", function() { - it ( "should construct given basic arguments", function() { - var uri = new mw.Uri( { protocol: 'http', host: 'www.foo.local', path: '/this' } ); - expect( uri.toString() ).toEqual( 'http://www.foo.local/this' ); - } ); - - it ( "should construct given more complex arguments", function() { - var uri = new mw.Uri( { - protocol: 'http', - host: 'www.foo.local', - path: '/this', - query: { hi: 'there' }, - fragment: 'blah' - } ); - expect( uri.toString() ).toEqual( 'http://www.foo.local/this?hi=there#blah' ); - } ); - - it ( "should fail to construct without required properties", function() { - expect( function() { - var uri = new mw.Uri( { protocol: 'http', host: 'www.foo.local' } ); - } ).toThrow( "Bad constructor arguments" ); - } ); - } ); - - describe( "should be able to manipulate properties", function() { - var uri; - - beforeEach( function() { - uri = new mw.Uri( 'http://en.wiki.local/w/api.php' ); - } ); - - it( "can add a fragment", function() { - uri.fragment = 'frag'; - expect( uri.toString() ).toEqual( 'http://en.wiki.local/w/api.php#frag' ); - } ); - - it( "can change host and port", function() { - uri.host = 'fr.wiki.local'; - uri.port = '8080'; - expect( uri.toString() ).toEqual( 'http://fr.wiki.local:8080/w/api.php' ); - } ); - - it ( "can add query arguments", function() { - uri.query.foo = 'bar'; - expect( uri.toString() ).toEqual( 'http://en.wiki.local/w/api.php?foo=bar' ); - } ); - - it ( "can extend query arguments", function() { - uri.query.foo = 'bar'; - expect( uri.toString() ).toEqual( 'http://en.wiki.local/w/api.php?foo=bar' ); - uri.extend( { foo: 'quux', pif: 'paf' } ); - expect( uri.toString() ).toContain( 'foo=quux' ); - expect( uri.toString() ).not.toContain( 'foo=bar' ); - expect( uri.toString() ).toContain( 'pif=paf' ); - } ); - - it ( "can remove query arguments", function() { - uri.query.foo = 'bar'; - expect( uri.toString() ).toEqual( 'http://en.wiki.local/w/api.php?foo=bar' ); - delete( uri.query.foo ); - expect( uri.toString() ).toEqual( 'http://en.wiki.local/w/api.php' ); - } ); - - } ); - - describe( "should handle protocol-relative URLs", function() { - - it ( "should create protocol-relative URLs with same protocol as document", function() { - var uriRel = mw.UriRelative( 'glork://en.wiki.local/foo.php' ); - var uri = new uriRel( '//en.wiki.local/w/api.php' ); - expect( uri.protocol ).toEqual( 'glork' ); - } ); - - } ); - - it( "should throw error on no arguments to constructor", function() { - expect( function() { - var uri = new mw.Uri(); - } ).toThrow( "Bad constructor arguments" ); - } ); - - it( "should throw error on empty string as argument to constructor", function() { - expect( function() { - var uri = new mw.Uri( '' ); - } ).toThrow( "Bad constructor arguments" ); - } ); - - it( "should throw error on non-URI as argument to constructor", function() { - expect( function() { - var uri = new mw.Uri( 'glaswegian penguins' ); - } ).toThrow( "Bad constructor arguments" ); - } ); - - it( "should throw error on improper URI as argument to constructor", function() { - expect( function() { - var uri = new mw.Uri( 'http:/foo.com' ); - } ).toThrow( "Bad constructor arguments" ); - } ); - - it( "should throw error on URI without protocol or // in strict mode", function() { - expect( function() { - var uri = new mw.Uri( 'foo.com/bar/baz', true ); - } ).toThrow( "Bad constructor arguments" ); - } ); - - it( "should normalize URI without protocol or // in loose mode", function() { - var uri = new mw.Uri( 'foo.com/bar/baz', false ); - expect( uri.toString() ).toEqual( 'http://foo.com/bar/baz' ); - } ); - - } ); - -} )(); diff --git a/tests/jasmine/spec_makers/makeJqueryMsgSpec.php b/tests/jasmine/spec_makers/makeJqueryMsgSpec.php index 1ac8dcba..840da96a 100644 --- a/tests/jasmine/spec_makers/makeJqueryMsgSpec.php +++ b/tests/jasmine/spec_makers/makeJqueryMsgSpec.php @@ -8,13 +8,13 @@ * which can be used with the JasmineBDD framework. This specification can then be used by simply including it into * the SpecRunner.html file. * - * This is similar to Michael Dale (mdale@mediawiki.org)'s parser tests, except that it doesn't look up the + * This is similar to Michael Dale (mdale@mediawiki.org)'s parser tests, except that it doesn't look up the * API results while doing the test, so the Jasmine run is much faster(at the cost of being out of date in rare * circumstances. But mostly the parsing that we are doing in Javascript doesn't change much.) * - */ + */ -$maintenanceDir = dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) ) . '/maintenance'; +$maintenanceDir = dirname( dirname( dirname( __DIR__ ) ) ) . '/maintenance'; require( "$maintenanceDir/Maintenance.php" ); @@ -51,26 +51,22 @@ class MakeLanguageSpec extends Maintenance { private function getMessagesAndTests() { $messages = array(); $tests = array(); - $wfMsgExtOptions = array( 'parsemag' ); foreach ( array( 'en', 'fr', 'ar', 'jp', 'zh' ) as $languageCode ) { - $wfMsgExtOptions['language'] = $languageCode; foreach ( self::$keyToTestArgs as $key => $testArgs ) { foreach ($testArgs as $args) { // get the raw template, without any transformations - $template = wfMsgGetKey( $key, /* useDb */ true, $languageCode, /* transform */ false ); + $template = wfMessage( $key )->inLanguage( $languageCode )->plain(); - // get the magic-parsed version with args - $wfMsgExtArgs = array_merge( array( $key, $wfMsgExtOptions ), $args ); - $result = call_user_func_array( 'wfMsgExt', $wfMsgExtArgs ); + $result = wfMessage( $key, $args )->inLanguage( $languageCode )->text(); // record the template, args, language, and expected result - // fake multiple languages by flattening them together + // fake multiple languages by flattening them together $langKey = $languageCode . '_' . $key; $messages[ $langKey ] = $template; - $tests[] = array( + $tests[] = array( 'name' => $languageCode . " " . $key . " " . join( ",", $args ), 'key' => $langKey, - 'args' => $args, + 'args' => $args, 'result' => $result, 'lang' => $languageCode ); diff --git a/tests/parser/parserTest.inc b/tests/parser/parserTest.inc index 30e451b3..86e1e192 100644 --- a/tests/parser/parserTest.inc +++ b/tests/parser/parserTest.inc @@ -140,7 +140,7 @@ class ParserTest { $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo, $parserMemc, $wgThumbnailScriptPath, $wgScriptPath, $wgArticlePath, $wgStyleSheetPath, $wgScript, $wgStylePath, $wgExtensionAssetsPath, - $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; + $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgLockManagers; $wgScript = '/index.php'; $wgScriptPath = '/'; @@ -149,6 +149,11 @@ class ParserTest { $wgStylePath = '/skins'; $wgExtensionAssetsPath = '/extensions'; $wgThumbnailScriptPath = false; + $wgLockManagers = array( array( + 'name' => 'fsLockManager', + 'class' => 'FSLockManager', + 'lockDirectory' => wfTempDir() . '/test-repo/lockdir', + ) ); $wgLocalFileRepo = array( 'class' => 'LocalRepo', 'name' => 'local', @@ -627,6 +632,11 @@ class ParserTest { 'wgScriptPath' => '/', 'wgArticlePath' => '/wiki/$1', 'wgActionPaths' => array(), + 'wgLockManagers' => array( + 'name' => 'fsLockManager', + 'class' => 'FSLockManager', + 'lockDirectory' => $this->uploadDir . '/lockdir', + ), 'wgLocalFileRepo' => array( 'class' => 'LocalRepo', 'name' => 'local', @@ -680,7 +690,6 @@ class ParserTest { 'wgExternalLinkTarget' => false, 'wgAlwaysUseTidy' => false, 'wgHtml5' => true, - 'wgCleanupPresentationalAttributes' => true, 'wgWellFormedXml' => true, 'wgAllowMicrodataAttributes' => true, 'wgAdaptiveMessageCache' => true, @@ -700,6 +709,9 @@ class ParserTest { $this->savedGlobals = array(); + /** @since 1.20 */ + wfRunHooks( 'ParserTestGlobals', array( &$settings ) ); + foreach ( $settings as $var => $val ) { if ( array_key_exists( $var, $GLOBALS ) ) { $this->savedGlobals[$var] = $GLOBALS[$var]; @@ -951,6 +963,8 @@ class ParserTest { */ private function teardownGlobals() { RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + LockManagerGroup::destroySingleton(); LinkCache::singleton()->clear(); foreach ( $this->savedGlobals as $var => $val ) { diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt index 11a55163..df057248 100644 --- a/tests/parser/parserTests.txt +++ b/tests/parser/parserTests.txt @@ -59,6 +59,12 @@ MediaWiki:bad image list * [[File:Bad.jpg]] except [[Nasty page]] !!endarticle +!! article +Template:inner list +!! text +* item 1 +!! endarticle + ### ### Basic tests ### @@ -79,6 +85,29 @@ This is a simple paragraph. !! end !! test +Paragraphs with extra newline spacing +!! input +foo + +bar + + +baz + + + +booz +!! result +<p>foo +</p><p>bar +</p><p><br /> +baz +</p><p><br /> +</p><p>booz +</p> +!! end + +!! test Simple list !! input * Item 1 @@ -129,6 +158,285 @@ Italics and bold !! end ### +### 2-quote opening sequence tests +### +!! test +Italics and bold: 2-quote opening sequence: (2,2) +!! input +''foo'' +!! result +<p><i>foo</i> +</p> +!!end + + +!! test +Italics and bold: 2-quote opening sequence: (2,3) +!! input +''foo''' +!! result +<p><i>foo'</i> +</p> +!!end + + +!! test +Italics and bold: 2-quote opening sequence: (2,4) +!! input +''foo'''' +!! result +<p><i>foo''</i> +</p> +!!end + + +!! test +Italics and bold: 2-quote opening sequence: (2,5) +!! input +''foo''''' +!! result +<p><i>foo</i> +</p> +!!end + + +### +### 3-quote opening sequence tests +### + +!! test +Italics and bold: 3-quote opening sequence: (3,2) +!! input +'''foo'' +!! result +<p>'<i>foo</i> +</p> +!!end + + +!! test +Italics and bold: 3-quote opening sequence: (3,3) +!! input +'''foo''' +!! result +<p><b>foo</b> +</p> +!!end + + +!! test +Italics and bold: 3-quote opening sequence: (3,4) +!! input +'''foo'''' +!! result +<p><b>foo'</b> +</p> +!!end + + +!! test +Italics and bold: 3-quote opening sequence: (3,5) +!! input +'''foo''''' +!! result +<p><b>foo</b> +</p> +!!end + + +### +### 4-quote opening sequence tests +### + +!! test +Italics and bold: 4-quote opening sequence: (4,2) +!! input +''''foo'' +!! result +<p>''<i>foo</i> +</p> +!!end + + +!! test +Italics and bold: 4-quote opening sequence: (4,3) +!! input +''''foo''' +!! result +<p>'<b>foo</b> +</p> +!!end + + +!! test +Italics and bold: 4-quote opening sequence: (4,4) +!! input +''''foo'''' +!! result +<p>'<b>foo'</b> +</p> +!!end + + +!! test +Italics and bold: 4-quote opening sequence: (4,5) +!! input +''''foo''''' +!! result +<p>'<b>foo</b> +</p> +!!end + + +### +### 5-quote opening sequence tests +### + +!! test +Italics and bold: 5-quote opening sequence: (5,2) +!! input +'''''foo'' +!! result +<p><b><i>foo</i></b> +</p> +!!end + + +!! test +Italics and bold: 5-quote opening sequence: (5,3) +!! input +'''''foo''' +!! result +<p><i><b>foo</b></i> +</p> +!!end + + +!! test +Italics and bold: 5-quote opening sequence: (5,4) +!! input +'''''foo'''' +!! result +<p><i><b>foo'</b></i> +</p> +!!end + + +!! test +Italics and bold: 5-quote opening sequence: (5,5) +!! input +'''''foo''''' +!! result +<p><i><b>foo</b></i> +</p> +!!end + +### +### multiple quote sequences in a line +### +!! test +Italics and bold: multiple quote sequences: (2,4,2) +!! input +''foo''''bar'' +!! result +<p><i>foo'<b>bar</b></i> +</p> +!!end + + +!! test +Italics and bold: multiple quote sequences: (2,4,3) +!! input +''foo''''bar''' +!! result +<p><i>foo'<b>bar</b></i> +</p> +!!end + + +!! test +Italics and bold: multiple quote sequences: (2,4,4) +!! input +''foo''''bar'''' +!! result +<p><i>foo'<b>bar'</b></i> +</p> +!!end + + +!! test +Italics and bold: multiple quote sequences: (3,4,2) +!! input +'''foo''''bar'' +!! result +<p><b>foo'</b>bar +</p> +!!end + + +!! test +Italics and bold: multiple quote sequences: (3,4,3) +!! input +'''foo''''bar''' +!! result +<p><b>foo'</b>bar +</p> +!!end + +### +### other quote tests +### +!! test +Italics and bold: other quote tests: (2,3,5) +!! input +''this is about '''foo's family''''' +!! result +<p><i>this is about <b>foo's family</b></i> +</p> +!!end + + +!! test +Italics and bold: other quote tests: (2,(3,3),2) +!! input +''this is about '''foo's''' family'' +!! result +<p><i>this is about <b>foo's</b> family</i> +</p> +!!end + + +!! test +Italics and bold: other quote tests: (3,2,3,2) +!! input +'''this is about ''foo'''s family'' +!! result +<p><b>this is about <i>foo</i></b><i>s family</i> +</p> +!!end + + +!! test +Italics and bold: other quote tests: (3,2,3,3) +!! input +'''this is about ''foo'''s family''' +!! result +<p>'<i>this is about </i>foo<b>s family</b> +</p> +!!end + + + +!! test +Italics and bold: other quote tests: (3,(2,2),3) +!! input +'''this is about ''foo's'' family''' +!! result +<p><b>this is about <i>foo's</i> family</b> +</p> +!!end + +### ### <nowiki> test cases ### @@ -634,6 +942,388 @@ Definition and unordered list using wiki syntax nested in unordered list using h !! end +!! test +Definition list with empty definition and following paragraph +!! input +; term: +Paragraph text +!! result +<dl><dt> term</dt><dd> +</dd></dl> +<p>Paragraph text +</p> +!! end + +!! test +Definition Lists: No nesting: Multiple dd's +!! input +;x +:a +:b +!! result +<dl><dt>x +</dt><dd>a +</dd><dd>b +</dd></dl> + +!! end + +!! test +Definition Lists: Indentation: Regular +!! input +:i1 +::i2 +:::i3 +!! result +<dl><dd>i1 +<dl><dd>i2 +<dl><dd>i3 +</dd></dl> +</dd></dl> +</dd></dl> + +!! end + +!! test +Definition Lists: Indentation: Missing 1st level +!! input +::i2 +:::i3 +!! result +<dl><dd><dl><dd>i2 +<dl><dd>i3 +</dd></dl> +</dd></dl> +</dd></dl> + +!! end + +!! test +Definition Lists: Indentation: Multi-level indent +!! input +:::i3 +!! result +<dl><dd><dl><dd><dl><dd>i3 +</dd></dl> +</dd></dl> +</dd></dl> + +!! end + +## The PHP parser treats : items (dd) without a corresponding ; item (dt) +## as an empty dt item. It also ignores all but the last ";" when followed +## by ":" later on. So, ";" are not ignored in ";;;t3" but are ignored in +## ";;;t3 :d1". So, PHP parser behavior is a little inconsistent wrt multiple +## ";"s. +## +## Ex: ";;t2 ::d2" is transformed into: +## +## <dl> +## <dt>t2 </dt> +## <dd> +## <dl> +## <dt></dt> +## <dd>d2</dd> +## </dl> +## </dd> +## </dl> +## +## But, Parsoid treats "; :" as a tight atomic unit and excess ":" as plain text +## So, the same wikitext above (;;t2 ::d2) is transformed into: +## +## <dl> +## <dt> +## <dl> +## <dt>t2 </dt> +## <dd>:d2</dd> +## </dl> +## </dt> +## </dl> +## +## All Parsoid only definition list tests have this difference. +## +## See also: https://bugzilla.wikimedia.org/show_bug.cgi?id=6569 +## and http://lists.wikimedia.org/pipermail/wikitext-l/2011-November/000483.html + +!! test +Definition Lists: Nesting: Multi-level (Parsoid only) +!! options +disabled +!! input +;t1 :d1 +;;t2 ::d2 +;;;t3 :::d3 +!! result +<dl> + <dt>t1 </dt> + <dd>d1</dd> + <dt> + <dl> + <dt>t2 </dt> + <dd>:d2</dd> + <dt> + <dl> + <dt>t3 </dt> + <dd>::d3</dd> + </dl> + </dt> + </dl> + </dt> +</dl> + + +!! end + + +!! test +Definition Lists: Nesting: Test 2 (Parsoid only) +!! options +disabled +!! input +;t1 +::d2 +!! result +<dl> + <dt>t1</dt> + <dd> + <dl> + <dd>d2</dd> + </dl> + </dd> +</dl> + +!! end + + +!! test +Definition Lists: Nesting: Test 3 (Parsoid only) +!! options +disabled +!! input +:;t1 +::::d2 +!! result +<dl> + <dd> + <dl> + <dt>t1</dt> + <dd> + <dl> + <dd> + <dl> + <dd>d2</dd> + </dl> + </dd> + </dl> + </dd> + </dl> + </dd> +</dl> + +!! end + + +!! test +Definition Lists: Nesting: Test 4 +!! input +::;t3 +:::d3 +!! result +<dl><dd><dl><dd><dl><dt>t3 +</dt><dd>d3 +</dd></dl> +</dd></dl> +</dd></dl> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 1 +!! input +:;* foo +::* bar +:; baz +!! result +<dl><dd><dl><dt><ul><li> foo +</li><li> bar +</li></ul> +</dt></dl> +<dl><dt> baz +</dt></dl> +</dd></dl> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 2 +!! input +*: d1 +*: d2 +!! result +<ul><li><dl><dd> d1 +</dd><dd> d2 +</dd></dl> +</li></ul> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 3 +!! input +*::: d1 +*::: d2 +!! result +<ul><li><dl><dd><dl><dd><dl><dd> d1 +</dd><dd> d2 +</dd></dl> +</dd></dl> +</dd></dl> +</li></ul> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 4 +!! input +*;d1 :d2 +*;d3 :d4 +!! result +<ul><li><dl><dt>d1 </dt><dd>d2 +</dd><dt>d3 </dt><dd>d4 +</dd></dl> +</li></ul> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 5 +!! input +*:d1 +*:: d2 +!! result +<ul><li><dl><dd>d1 +<dl><dd> d2 +</dd></dl> +</dd></dl> +</li></ul> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 6 +!! input +#*:d1 +#*::: d3 +!! result +<ol><li><ul><li><dl><dd>d1 +<dl><dd><dl><dd> d3 +</dd></dl> +</dd></dl> +</dd></dl> +</li></ul> +</li></ol> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 7 +!! input +:* d1 +:* d2 +!! result +<dl><dd><ul><li> d1 +</li><li> d2 +</li></ul> +</dd></dl> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 8 +!! input +:* d1 +::* d2 +!! result +<dl><dd><ul><li> d1 +</li></ul> +<dl><dd><ul><li> d2 +</li></ul> +</dd></dl> +</dd></dl> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 9 +!! input +*;foo :bar +!! result +<ul><li><dl><dt>foo </dt><dd>bar +</dd></dl> +</li></ul> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 10 +!! input +*#;foo :bar +!! result +<ul><li><ol><li><dl><dt>foo </dt><dd>bar +</dd></dl> +</li></ol> +</li></ul> + +!! end + + +!! test +Definition Lists: Mixed Lists: Test 11 +!! input +*#*#;*;;foo :bar +*#*#;boo :baz +!! result +<ul><li><ol><li><ul><li><ol><li><dl><dt>foo </dt><dd><ul><li><dl><dt><dl><dt>bar +</dt></dl> +</dd></dl> +</li></ul> +</dd></dl> +<dl><dt>boo </dt><dd>baz +</dd></dl> +</li></ol> +</li></ul> +</li></ol> +</li></ul> + +!! end + + +!! test +Definition Lists: Weird Ones: Test 1 +!! input +*#;*::;; foo : bar (who uses this?) +!! result +<ul><li><ol><li><dl><dt> foo </dt><dd><ul><li><dl><dd><dl><dd><dl><dt><dl><dt> bar (who uses this?) +</dt></dl> +</dd></dl> +</dd></dl> +</dd></dl> +</li></ul> +</dd></dl> +</li></ol> +</li></ul> + +!! end ### ### External links @@ -914,6 +1604,23 @@ External links: [IDN ignored character reference in hostname; strip it right off </p> !! end +# FIXME: This test (the IDN characters in the text of a link) is an inconsistency. +# Where an external link could easily circumvent the sanitization of the text of +# a link like this (where an IDN-ignore character is in the URL somewhere), this +# test demands a higher standard. That's a bit strange. +# +# Example: +# +# http://example.com -> [http://example.com|http://example.com] +# [http://example.com|http://example.com] -> [http://example.com|http://example.com] +# +# The first example is sanitized, but the second is not. Any security benefits +# from this production are trivial to circumvent. Either remove this test and +# let the parser(s) do their thing unaccosted, or fix the inconsistency and change +# the test accordingly. +# +# All our love, +# The Parsoid team. !! test External links: IDN ignored character reference in hostname; strip it right off !! input @@ -1252,6 +1959,8 @@ Normal text. '''This year''''s election ''should'' beat '''last year''''s. ''Tom'''s car is bigger than ''Susan'''s. + +Plain ''italic'''s plain !! result <p><i><b>Bold italic text </b>with bold deactivated<b> in between.</b></i> </p><p><b><i>Bold italic text </i>with italic deactivated<i> in between.</i></b> @@ -1262,6 +1971,7 @@ Normal text. </p><p>Normal text. </p><p><b>This year'</b>s election <i>should</i> beat <b>last year'</b>s. </p><p><i>Tom<b>s car is bigger than </b></i><b>Susan</b>s. +</p><p>Plain <i>italic'</i>s plain </p> !! end @@ -2061,20 +2771,58 @@ Failing to transform badly formed HTML into correct XHTML </p> !!end -!! test +!! test Horizontal ruler (should it add that extra space?) -!! input +!! input <hr> <hr > foo <hr > bar -!! result +!! result <hr /> <hr /> foo <hr /> bar !! end +!! test +Horizontal ruler -- 4+ dashes render hr +!! input +---- +!! result +<hr /> + +!! end + +!! test +Horizontal ruler -- eats additional dashes on the same line +!! input +--------- +!! result +<hr /> + +!! end + +!! test +Horizontal ruler -- does not collaps dashes on consecutive lines +!! input +---- +---- +!! result +<hr /> +<hr /> + +!! end + +!! test +Horizontal ruler -- <4 dashes render as plain text +!! input +--- +!! result +<p>--- +</p> +!! end + ### ### Block-level elements ### @@ -2122,6 +2870,8 @@ Mixed list **#Number on level 3 *#number level 2 *Level 1 +*** Level 3 +#** Level 3, but ordered !! result <ul><li>Mixed list <ol><li> with numbers @@ -2144,11 +2894,127 @@ Mixed list <ol><li>number level 2 </li></ol> </li><li>Level 1 +<ul><li><ul><li> Level 3 </li></ul> +</li></ul> +</li></ul> +<ol><li><ul><li><ul><li> Level 3, but ordered +</li></ul> +</li></ul> +</li></ol> !! end !! test +Nested lists 1 +!! input +*foo +**bar +!! result +<ul><li>foo +<ul><li>bar +</li></ul> +</li></ul> + +!! end + +!! test +Nested lists 2 +!! input +**foo +*bar +!! result +<ul><li><ul><li>foo +</li></ul> +</li><li>bar +</li></ul> + +!! end + +!! test +Nested lists 3 (first element empty) +!! input +* +**bar +!! result +<ul><li> +<ul><li>bar +</li></ul> +</li></ul> + +!! end + +!! test +Nested lists 4 (first element empty) +!! input +** +*bar +!! result +<ul><li><ul><li> +</li></ul> +</li><li>bar +</li></ul> + +!! end + +!! test +Nested lists 5 (both elements empty) +!! input +** +* +!! result +<ul><li><ul><li> +</li></ul> +</li><li> +</li></ul> + +!! end + +!! test +Nested lists 6 (both elements empty) +!! input +* +** +!! result +<ul><li> +<ul><li> +</li></ul> +</li></ul> + +!! end + +!! test +Nested lists 7 (skip initial nesting levels) +!! input +*** foo +!! result +<ul><li><ul><li><ul><li> foo +</li></ul> +</li></ul> +</li></ul> + +!! end + +!! test +Nested lists 8 (multiple nesting transitions) +!! input +* foo +*** bar +** baz +* boo +!! result +<ul><li> foo +<ul><li><ul><li> bar +</li></ul> +</li><li> baz +</li></ul> +</li><li> boo +</li></ul> + +!! end + + +!! test List items are not parsed correctly following a <pre> block (bug 785) !! input * <pre>foo</pre> @@ -2162,6 +3028,57 @@ List items are not parsed correctly following a <pre> block (bug 785) !! end +!! test +List items from template +!! input + +{{inner list}} +* item 2 + +* item 0 +{{inner list}} +* item 2 + +* item 0 +* notSOL{{inner list}} +* item 2 +!! result +<ul><li> item 1 +</li><li> item 2 +</li></ul> +<ul><li> item 0 +</li><li> item 1 +</li><li> item 2 +</li></ul> +<ul><li> item 0 +</li><li> notSOL +</li><li> item 1 +</li><li> item 2 +</li></ul> + +!! end + +!! test +List interrupted by empty line or heading +!! input +* foo + +** bar +== A heading == +* Another list item +!! result +<ul><li> foo +</li></ul> +<ul><li><ul><li> bar +</li></ul> +</li></ul> +<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: A heading">edit</a>]</span> <span class="mw-headline" id="A_heading"> A heading </span></h2> +<ul><li> Another list item +</li></ul> + +!!end + + ### ### Magic Words ### @@ -2310,6 +3227,17 @@ title=[[User:Ævar Arnfjörð Bjarmason]] !! end !! test +Magic Word: {{NAMESPACENUMBER}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{NAMESPACENUMBER}} +!! result +<p>2 +</p> +!! end + +!! test Magic Word: {{NUMBEROFFILES}} !! input {{NUMBEROFFILES}} @@ -3482,15 +4410,15 @@ pst [[Bar:Article(context)|]] [[:Bar:Article(context)|]] [[|Article(context)]] -[[Bar:X (Y) Z|]] -[[:Bar:X (Y) Z|]] +[[Bar:X(Y)Z|]] +[[:Bar:X(Y)Z|]] !! result [[Article(context)|Article]] [[Bar:Article(context)|Article]] [[:Bar:Article(context)|Article]] [[Article(context)]] -[[Bar:X (Y) Z|X (Y) Z]] -[[:Bar:X (Y) Z|X (Y) Z]] +[[Bar:X(Y)Z|X(Y)Z]] +[[:Bar:X(Y)Z|X(Y)Z]] !! end !! test @@ -3534,6 +4462,26 @@ pst !! end !! test +pre-save transform: context links ("pipe trick") with commas (bug 21660) +!! options +pst +!! input +[[Article (context), context|]] +[[Article (context),context|]] +[[Bar:Article (context), context|]] +[[Bar:Article (context),context|]] +[[:Bar:Article (context), context|]] +[[:Bar:Article (context),context|]] +!! result +[[Article (context), context|Article]] +[[Article (context),context|Article]] +[[Bar:Article (context), context|Article]] +[[Bar:Article (context),context|Article]] +[[:Bar:Article (context), context|Article]] +[[:Bar:Article (context),context|Article]] +!! end + +!! test pre-save transform: trim trailing empty lines !! options pst @@ -3681,6 +4629,36 @@ msg No such special page !! end +!! test +{{#speciale:}} page name, known +!! options +msg +!! input +{{#speciale:Recentchanges}} +!! result +Special:RecentChanges +!! end + +!! test +{{#speciale:}} page name with subpage, known +!! options +msg +!! input +{{#speciale:Recentchanges/param}} +!! result +Special:RecentChanges/param +!! end + +!! test +{{#speciale:}} page name, unknown +!! options +msg +!! input +{{#speciale:foobarnonexistent}} +!! result +No_such_special_page +!! end + ### ### Images ### @@ -3734,7 +4712,7 @@ Image with link parameter, URL target !! input [[Image:foobar.jpg|link=http://example.com/]] !! result -<p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +<p><a href="http://example.com/" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -3745,7 +4723,29 @@ Image with link parameter, wgExternalLinkTarget !! config wgExternalLinkTarget='foobar' !! result -<p><a href="http://example.com/" target="foobar"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +<p><a href="http://example.com/" target="foobar" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +</p> +!! end + +!! test +Image with link parameter, wgNoFollowLinks set to false +!! input +[[Image:foobar.jpg|link=http://example.com/]] +!! config +wgNoFollowLinks=false +!! result +<p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +</p> +!! end + +!! test +Image with link parameter, wgNoFollowDomainExceptions +!! input +[[Image:foobar.jpg|link=http://example.com/]] +!! config +wgNoFollowDomainExceptions='example.com' +!! result +<p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -3756,7 +4756,7 @@ Image with link parameter, wgExternalLinkTarget, unnamed parameter !! config wgExternalLinkTarget='foobar' !! result -<p><a href="http://example.com/" title="Title" target="foobar"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +<p><a href="http://example.com/" title="Title" target="foobar" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -3783,7 +4783,7 @@ Image with link parameter (URL target) and unnamed parameter !! input [[Image:foobar.jpg|link=http://example.com/|Title]] !! result -<p><a href="http://example.com/" title="Title"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +<p><a href="http://example.com/" title="Title" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -3999,6 +4999,15 @@ Bug 3090: External links other than http: in image captions !! end +!! test +Custom class +!! input +[[Image:foobar.jpg|a|class=b]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="a"><img alt="a" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="b" /></a> +</p> +!! end + !! article File:Barfoo.jpg !! text @@ -4524,6 +5533,30 @@ section 5 !! end !! test +Headers with excess '=' characters +(Are similar tests necessary beyond the 1st level?) +!! input +=foo== +==foo= +=''italic'' heading== +==''italic'' heading= +!! result +<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div> +<ul> +<li class="toclevel-1 tocsection-1"><a href="#foo.3D"><span class="tocnumber">1</span> <span class="toctext">foo=</span></a></li> +<li class="toclevel-1 tocsection-2"><a href="#.3Dfoo"><span class="tocnumber">2</span> <span class="toctext">=foo</span></a></li> +<li class="toclevel-1 tocsection-3"><a href="#italic_heading.3D"><span class="tocnumber">3</span> <span class="toctext"><i>italic</i> heading=</span></a></li> +<li class="toclevel-1 tocsection-4"><a href="#.3Ditalic_heading"><span class="tocnumber">4</span> <span class="toctext">=<i>italic</i> heading</span></a></li> +</ul> +</td></tr></table> +<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: foo=">edit</a>]</span> <span class="mw-headline" id="foo.3D">foo=</span></h1> +<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: =foo">edit</a>]</span> <span class="mw-headline" id=".3Dfoo">=foo</span></h1> +<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: italic heading=">edit</a>]</span> <span class="mw-headline" id="italic_heading.3D"><i>italic</i> heading=</span></h1> +<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: =italic heading">edit</a>]</span> <span class="mw-headline" id=".3Ditalic_heading">=<i>italic</i> heading</span></h1> + +!! end + +!! test BUG 1219 URL next to image (broken) !! input http://example.com[[Image:foobar.jpg]] @@ -6002,9 +7035,7 @@ Special page transclusion !! input {{Special:Prefixindex/Xyzzyx}} !! result -<p><br /> -</p> -<table border="0" id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table> +<table id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table> !! end @@ -6015,12 +7046,8 @@ Special page transclusion twice (bug 5021) {{Special:Prefixindex/Xyzzyx}} {{Special:Prefixindex/Xyzzyx}} !! result -<p><br /> -</p> -<table border="0" id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table> -<p><br /> -</p> -<table border="0" id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table> +<table id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table> +<table id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table> !! end @@ -8449,6 +9476,17 @@ wgUseDynamicDates=true </p> !! end +!! test +formatdate parser function, with default format and on a page of which the content language is always English and different from the wiki content language +!! options +language=nl title=[[MediaWiki:Common.css]] +!! input +{{#formatdate:2009-03-24|dmy}} +!! result +<p><span class="mw-formatted-date" title="2009-03-24">24 March 2009</span> +</p> +!! end + # # # @@ -8897,22 +9935,6 @@ Bug 31098 Template which includes system messages which includes the template !! end !! test -Deprecated presentational attributes are converted to css -!! input -{| -| valign=top align=left width=100 height=25% | Asdf -|} -<ul type="disc"></ul> -!! result -<table> -<tr> -<td style="text-align: left; height: 25%; vertical-align: top; width: 100px;"> Asdf -</td></tr></table> -<ul style="list-style-type: disc;"></ul> - -!! end - -!! test Bug31490 Turkish: ucfirst 'blah' !! options language=tr @@ -9200,11 +10222,142 @@ nowiki inside link inside heading (bug 18295) !! end +!! test +new support for bdi element (bug 31817) +!! input +<p dir="rtl" lang="he">ולדימיר לנין (ברוסית: <bdi lang="ru">Владимир Ленин</bdi>, 24 באפריל 1870–22 בינואר 1924) הוא מנהיג פוליטי קומוניסטי רוסי.</p> +!! result +<p dir="rtl" lang="he">ולדימיר לנין (ברוסית: <bdi lang="ru">Владимир Ленин</bdi>, 24 באפריל 1870–22 בינואר 1924) הוא מנהיג פוליטי קומוניסטי רוסי.</p> + +!!end + +!! test +Ignore pipe between table row attributes +!! input +{| +| quux +|- id=foo | style='color: red' +| bar +|} +!! result +<table> +<tr> +<td> quux +</td></tr> +<tr id="foo" style="color: red"> +<td> bar +</td></tr></table> + +!! end + +!!test +Gallery override link with WikiLink (bug 34852) +!! input +<gallery> +File:foobar.jpg|caption|alt=galleryalt|link=InterWikiLink +</gallery> +!! result +<ul class="gallery"> + <li class="gallerybox" style="width: 155px"><div style="width: 155px"> + <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/InterWikiLink"><img alt="galleryalt" src="http://example.com/images/3/3a/Foobar.jpg" width="120" height="14" /></a></div></div> + <div class="gallerytext"> +<p>caption +</p> + </div> + </div></li> +</ul> + +!! end + +!!test +Gallery override link with absolute external link (bug 34852) +!! input +<gallery> +File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org +</gallery> +!! result +<ul class="gallery"> + <li class="gallerybox" style="width: 155px"><div style="width: 155px"> + <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/3/3a/Foobar.jpg" width="120" height="14" /></a></div></div> + <div class="gallerytext"> +<p>caption +</p> + </div> + </div></li> +</ul> + +!! end + +!!test +Gallery override link with malicious javascript (bug 34852) +!! input +<gallery> +File:foobar.jpg|caption|alt=galleryalt|link=" onclick="alert('malicious javascript code!'); +</gallery> +!! result +<ul class="gallery"> + <li class="gallerybox" style="width: 155px"><div style="width: 155px"> + <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/%22_onclick%3D%22alert(%27malicious_javascript_code!%27);"><img alt="galleryalt" src="http://example.com/images/3/3a/Foobar.jpg" width="120" height="14" /></a></div></div> + <div class="gallerytext"> +<p>caption +</p> + </div> + </div></li> +</ul> + +!! end + +!!test +Language parser function +!! input +{{#language:ar}} +!! result +<p>العربية +</p> +!! end + +!!test +Padleft and padright as substr +!! input +{{padleft:|3|abcde}} +{{padright:|3|abcde}} +!! result +<p>abc +abc +</p> +!! end + +!!test +Bug 34939 - Case insensitive link parsing ([HttP://]) +!! input +[HttP://MediaWiki.Org/] +!! result +<p><a rel="nofollow" class="external autonumber" href="HttP://MediaWiki.Org/">[1]</a> +</p> +!! end + +!!test +Bug 34939 - Case insensitive link parsing ([HttP:// title]) +!! input +[HttP://MediaWiki.Org/ MediaWiki] +!! result +<p><a rel="nofollow" class="external text" href="HttP://MediaWiki.Org/">MediaWiki</a> +</p> +!! end + +!!test +Bug 34939 - Case insensitive link parsing (HttP://) +!! input +HttP://MediaWiki.Org/ +!! result +<p><a rel="nofollow" class="external free" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a> +</p> +!! end + TODO: more images more tables -math character entities and much more Try for 100% code coverage diff --git a/tests/parserTests.php b/tests/parserTests.php index d930ac5a..4df9a618 100644 --- a/tests/parserTests.php +++ b/tests/parserTests.php @@ -27,7 +27,7 @@ $otions = array( 'quick', 'color', 'quiet', 'help', 'show-output', 'record', 'run-disabled' ); $optionsWithArgs = array( 'regex', 'filter', 'seed', 'setversion' ); -require_once( dirname( __FILE__ ) . '/../maintenance/commandLine.inc' ); +require_once( __DIR__ . '/../maintenance/commandLine.inc' ); if ( isset( $options['help'] ) ) { echo <<<ENDS diff --git a/tests/phpunit/MediaWikiLangTestCase.php b/tests/phpunit/MediaWikiLangTestCase.php index 783f0315..6dd8ea35 100644 --- a/tests/phpunit/MediaWikiLangTestCase.php +++ b/tests/phpunit/MediaWikiLangTestCase.php @@ -10,6 +10,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase { public function setUp() { global $wgLanguageCode, $wgLang, $wgContLang; + parent::setUp(); + self::$oldLang = $wgLang; self::$oldContLang = $wgContLang; @@ -23,6 +25,7 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase { $wgContLang = $wgLang = Language::factory( $wgLanguageCode ); MessageCache::singleton()->disable(); + } public function tearDown() { @@ -32,6 +35,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase { $wgContLang = self::$oldContLang; $wgLanguageCode = $wgContLang->getCode(); self::$oldContLang = self::$oldLang = null; + + parent::tearDown(); } } diff --git a/tests/phpunit/MediaWikiPHPUnitCommand.php b/tests/phpunit/MediaWikiPHPUnitCommand.php index ea385ad9..fca32515 100644 --- a/tests/phpunit/MediaWikiPHPUnitCommand.php +++ b/tests/phpunit/MediaWikiPHPUnitCommand.php @@ -37,7 +37,7 @@ class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command { # PHPUnit uses stream_resolve_include_path() internally # See bug 32022 set_include_path( - dirname( __FILE__ ) + __DIR__ .PATH_SEPARATOR . get_include_path() ); diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index 6ec8bdc7..1cc45e08 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -6,6 +6,11 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { public $runDisabled = false; /** + * @var Array of TestUser + */ + public static $users; + + /** * @var DatabaseBase */ protected $db; @@ -17,6 +22,15 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { private static $dbSetup = false; /** + * Holds the paths of temporary files/directories created through getNewTempFile, + * and getNewTempDirectory + * + * @var array + */ + private $tmpfiles = array(); + + + /** * Table name prefixes. Oracle likes it shorter. */ const DB_PREFIX = 'unittest_'; @@ -71,13 +85,77 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } + /** + * obtains a new temporary file name + * + * The obtained filename is enlisted to be removed upon tearDown + * + * @returns string: absolute name of the temporary file + */ + protected function getNewTempFile() { + $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); + $this->tmpfiles[] = $fname; + return $fname; + } + + /** + * obtains a new temporary directory + * + * The obtained directory is enlisted to be removed (recursively with all its contained + * files) upon tearDown. + * + * @returns string: absolute name of the temporary directory + */ + protected function getNewTempDirectory() { + // Starting of with a temporary /file/. + $fname = $this->getNewTempFile(); + + // Converting the temporary /file/ to a /directory/ + // + // The following is not atomic, but at least we now have a single place, + // where temporary directory creation is bundled and can be improved + unlink( $fname ); + $this->assertTrue( wfMkdirParents( $fname ) ); + return $fname; + } + + protected function tearDown() { + // Cleaning up temporary files + foreach ( $this->tmpfiles as $fname ) { + if ( is_file( $fname ) || ( is_link( $fname ) ) ) { + unlink( $fname ); + } elseif ( is_dir( $fname ) ) { + wfRecursiveRemoveDir( $fname ); + } + } + + // clean up open transactions + if( $this->needsDB() && $this->db ) { + while( $this->db->trxLevel() > 0 ) { + $this->db->rollback(); + } + } + + parent::tearDown(); + } + function dbPrefix() { return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; } function needsDB() { + # if the test says it uses database tables, it needs the database + if ( $this->tablesUsed ) { + return true; + } + + # if the test says it belongs to the Database group, it needs the database $rc = new ReflectionClass( $this ); - return strpos( $rc->getDocComment(), '@group Database' ) !== false; + if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { + return true; + } + + return false; } /** @@ -260,10 +338,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } - public static function disableInterwikis( $prefix, &$data ) { - return false; - } - /** * Don't throw a warning if $function is deprecated and called later * @@ -275,4 +349,199 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { wfDeprecated( $function ); wfRestoreWarnings(); } + + /** + * Asserts that the given database query yields the rows given by $expectedRows. + * The expected rows should be given as indexed (not associative) arrays, with + * the values given in the order of the columns in the $fields parameter. + * Note that the rows are sorted by the columns given in $fields. + * + * @since 1.20 + * + * @param $table String|Array the table(s) to query + * @param $fields String|Array the columns to include in the result (and to sort by) + * @param $condition String|Array "where" condition(s) + * @param $expectedRows Array - an array of arrays giving the expected rows. + * + * @throws MWException if this test cases's needsDB() method doesn't return true. + * Test cases can use "@group Database" to enable database test support, + * or list the tables under testing in $this->tablesUsed, or override the + * needsDB() method. + */ + protected function assertSelect( $table, $fields, $condition, Array $expectedRows ) { + if ( !$this->needsDB() ) { + throw new MWException( 'When testing database state, the test cases\'s needDB()' . + ' method should return true. Use @group Database or $this->tablesUsed.'); + } + + $db = wfGetDB( DB_SLAVE ); + + $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); + $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); + + $i = 0; + + foreach ( $expectedRows as $expected ) { + $r = $res->fetchRow(); + self::stripStringKeys( $r ); + + $i += 1; + $this->assertNotEmpty( $r, "row #$i missing" ); + + $this->assertEquals( $expected, $r, "row #$i mismatches" ); + } + + $r = $res->fetchRow(); + self::stripStringKeys( $r ); + + $this->assertFalse( $r, "found extra row (after #$i)" ); + } + + /** + * Utility method taking an array of elements and wrapping + * each element in it's own array. Useful for data providers + * that only return a single argument. + * + * @since 1.20 + * + * @param array $elements + * + * @return array + */ + protected function arrayWrap( array $elements ) { + return array_map( + function( $element ) { + return array( $element ); + }, + $elements + ); + } + + /** + * Assert that two arrays are equal. By default this means that both arrays need to hold + * the same set of values. Using additional arguments, order and associated key can also + * be set as relevant. + * + * @since 1.20 + * + * @param array $expected + * @param array $actual + * @param boolean $ordered If the order of the values should match + * @param boolean $named If the keys should match + */ + protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { + if ( !$ordered ) { + $this->objectAssociativeSort( $expected ); + $this->objectAssociativeSort( $actual ); + } + + if ( !$named ) { + $expected = array_values( $expected ); + $actual = array_values( $actual ); + } + + call_user_func_array( + array( $this, 'assertEquals' ), + array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) + ); + } + + /** + * Put each HTML element on its own line and then equals() the results + * + * Use for nicely formatting of PHPUnit diff output when comparing very + * simple HTML + * + * @since 1.20 + * + * @param String $expected HTML on oneline + * @param String $actual HTML on oneline + * @param String $msg Optional message + */ + protected function assertHTMLEquals( $expected, $actual, $msg='' ) { + $expected = str_replace( '>', ">\n", $expected ); + $actual = str_replace( '>', ">\n", $actual ); + + $this->assertEquals( $expected, $actual, $msg ); + } + + /** + * Does an associative sort that works for objects. + * + * @since 1.20 + * + * @param array $array + */ + protected function objectAssociativeSort( array &$array ) { + uasort( + $array, + function( $a, $b ) { + return serialize( $a ) > serialize( $b ) ? 1 : -1; + } + ); + } + + /** + * Utility function for eliminating all string keys from an array. + * Useful to turn a database result row as returned by fetchRow() into + * a pure indexed array. + * + * @since 1.20 + * + * @param $r mixed the array to remove string keys from. + */ + protected static function stripStringKeys( &$r ) { + if ( !is_array( $r ) ) { + return; + } + + foreach ( $r as $k => $v ) { + if ( is_string( $k ) ) { + unset( $r[$k] ); + } + } + } + + /** + * Asserts that the provided variable is of the specified + * internal type or equals the $value argument. This is useful + * for testing return types of functions that return a certain + * type or *value* when not set or on error. + * + * @since 1.20 + * + * @param string $type + * @param mixed $actual + * @param mixed $value + * @param string $message + */ + protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { + if ( $actual === $value ) { + $this->assertTrue( true, $message ); + } + else { + $this->assertType( $type, $actual, $message ); + } + } + + /** + * Asserts the type of the provided value. This can be either + * in internal type such as boolean or integer, or a class or + * interface the value extends or implements. + * + * @since 1.20 + * + * @param string $type + * @param mixed $actual + * @param string $message + */ + protected function assertType( $type, $actual, $message = '' ) { + if ( is_object( $actual ) ) { + $this->assertInstanceOf( $type, $actual, $message ); + } + else { + $this->assertInternalType( $type, $actual, $message ); + } + } + } diff --git a/tests/phpunit/StructureTest.php b/tests/phpunit/StructureTest.php index f967c18d..17ea06c4 100644 --- a/tests/phpunit/StructureTest.php +++ b/tests/phpunit/StructureTest.php @@ -20,6 +20,7 @@ class StructureTest extends MediaWikiTestCase { 'MediaWikiLangTestCase', 'MediaWikiTestCase', 'PHPUnit_Framework_TestCase', + 'DumpTestCase', ) ); $testClassRegex = "^class .* extends ($testClassRegex)"; $finder = "find $rootPath -name '*.php' '!' -name '*Test.php'" . @@ -39,11 +40,14 @@ class StructureTest extends MediaWikiTestCase { $results, array( $this, 'filterSuites' ) ); - + $strip = strlen( $rootPath ) - 1; + foreach( $results as $k => $v) { + $results[$k] = substr( $v, $strip ); + } $this->assertEquals( array(), $results, - 'Unit test file names must end with Test.' + "Unit test file in $rootPath must end with Test." ); } diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index b023fdcf..933767e7 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -11,15 +11,15 @@ if ( !defined( 'MW_PHPUNIT_TEST' ) ) { You are running these tests directly from phpunit. You may not have all globals correctly set. Running phpunit.php instead is recommended. EOF; - require_once ( dirname( __FILE__ ) . "/phpunit.php" ); + require_once ( __DIR__ . "/phpunit.php" ); } // Output a notice when running with older versions of PHPUnit -if ( !version_compare( PHPUnit_Runner_Version::id(), "3.4.1", ">" ) ) { +if ( version_compare( PHPUnit_Runner_Version::id(), "3.6.7", "<" ) ) { echo <<<EOF ******************************************************************************** -These tests run best with version PHPUnit 3.4.2 or better. Earlier versions may +These tests run best with version PHPUnit 3.6.7 or better. Earlier versions may show failures because earlier versions of PHPUnit do not properly implement dependencies. diff --git a/tests/phpunit/data/media/exif-gps.jpg b/tests/phpunit/data/media/exif-gps.jpg Binary files differindex f99b484d..40137340 100644 --- a/tests/phpunit/data/media/exif-gps.jpg +++ b/tests/phpunit/data/media/exif-gps.jpg diff --git a/tests/phpunit/data/xmp/gps.result.php b/tests/phpunit/data/xmp/gps.result.php new file mode 100644 index 00000000..2d1243d5 --- /dev/null +++ b/tests/phpunit/data/xmp/gps.result.php @@ -0,0 +1,12 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'GPSAltitude' => -3.14159265301, + 'GPSDOP' => '5/1', + 'GPSLatitude' => 88.51805555, + 'GPSLongitude' => -21.12356945, + 'GPSVersionID' => '2.2.0.0' + ) +); + diff --git a/tests/phpunit/data/xmp/gps.xmp b/tests/phpunit/data/xmp/gps.xmp new file mode 100644 index 00000000..e52d2c8a --- /dev/null +++ b/tests/phpunit/data/xmp/gps.xmp @@ -0,0 +1,17 @@ +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> + + <rdf:Description rdf:about='' + xmlns:exif='http://ns.adobe.com/exif/1.0/'> + <exif:GPSAltitude>103993/33102</exif:GPSAltitude> + <exif:GPSAltitudeRef>1</exif:GPSAltitudeRef> + <exif:GPSDOP>5/1</exif:GPSDOP> + <exif:GPSLatitude>88,31.083333N</exif:GPSLatitude> + <exif:GPSLongitude>21,7.414167W</exif:GPSLongitude> + <exif:GPSVersionID>2.2.0.0</exif:GPSVersionID> + </rdf:Description> + +</rdf:RDF> +</x:xmpmeta> +<?xpacket end='w'?> diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php new file mode 100644 index 00000000..ce65d494 --- /dev/null +++ b/tests/phpunit/docs/ExportDemoTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Test for the demo xml + * + * @group Dump + */ +class ExportDemoTest extends DumpTestCase { + + /** + * @group large + */ + function testExportDemo() { + $this->validateXmlFileAgainstXsd( "../../docs/export-demo.xml" ); + } + + /** + * Validates a xml file against the xsd. + * + * The validation is slow, because php has to read the xsd on each call. + * + * @param $fname string: name of file to validate + */ + protected function validateXmlFileAgainstXsd( $fname ) { + $version = WikiExporter::schemaVersion(); + + $dom = new DomDocument(); + $dom->load( $fname ); + + try { + $this->assertTrue( $dom->schemaValidate( "../../docs/export-" . $version . ".xsd" ), + "schemaValidate has found an error" ); + } catch( Exception $e ) { + $this->fail( "xml not valid against xsd: " . $e->getMessage() ); + } + } +} diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php index 02571b55..17cee6e8 100644 --- a/tests/phpunit/includes/ArticleTablesTest.php +++ b/tests/phpunit/includes/ArticleTablesTest.php @@ -17,14 +17,14 @@ class ArticleTablesTest extends MediaWikiLangTestCase { $wgLang = Language::factory( 'fr' ); $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', 0, false, $user ); - $templates1 = $page->getUsedTemplates(); + $templates1 = $title->getTemplateLinksFrom(); $wgLang = Language::factory( 'de' ); $page->mPreparedEdit = false; // In order to force the rerendering of the same wikitext // We need an edit, a purge is not enough to regenerate the tables $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', EDIT_UPDATE, false, $user ); - $templates2 = $page->getUsedTemplates(); + $templates2 = $title->getTemplateLinksFrom(); $this->assertEquals( $templates1, $templates2 ); $this->assertEquals( $templates1[0]->getFullText(), 'Historial' ); diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php index 749f40b4..0c95b8d1 100644 --- a/tests/phpunit/includes/BlockTest.php +++ b/tests/phpunit/includes/BlockTest.php @@ -67,7 +67,7 @@ class BlockTest extends MediaWikiLangTestCase { // $this->dumpBlocks(); $this->assertTrue( $this->block->equals( Block::newFromTarget('UTBlockee') ), "newFromTarget() returns the same block as the one that was made"); - + $this->assertTrue( $this->block->equals( Block::newFromID( $this->blockId ) ), "newFromID() returns the same block as the one that was made"); } @@ -122,4 +122,109 @@ class BlockTest extends MediaWikiLangTestCase { array( false ) ); } + + function testBlockedUserCanNotCreateAccount() { + $username = 'BlockedUserToCreateAccountWith'; + $u = User::newFromName( $username ); + $u->setPassword( 'NotRandomPass' ); + $u->addToDatabase(); + unset( $u ); + + + // Sanity check + $this->assertNull( + Block::newFromTarget( $username ), + "$username should not be blocked" + ); + + // Reload user + $u = User::newFromName( $username ); + $this->assertFalse( + $u->isBlockedFromCreateAccount(), + "Our sandbox user should be able to create account before being blocked" + ); + + // Foreign perspective (blockee not on current wiki)... + $block = new Block( + /* $address */ $username, + /* $user */ 14146, + /* $by */ 0, + /* $reason */ 'crosswiki block...', + /* $timestamp */ wfTimestampNow(), + /* $auto */ false, + /* $expiry */ $this->db->getInfinity(), + /* anonOnly */ false, + /* $createAccount */ true, + /* $enableAutoblock */ true, + /* $hideName (ipb_deleted) */ true, + /* $blockEmail */ true, + /* $allowUsertalk */ false, + /* $byName */ 'MetaWikiUser' + ); + $block->insert(); + + // Reload block from DB + $userBlock = Block::newFromTarget( $username ); + $this->assertTrue( + (bool) $block->prevents( 'createaccount' ), + "Block object in DB should prevents 'createaccount'" + ); + + $this->assertInstanceOf( + 'Block', + $userBlock, + "'$username' block block object should be existent" + ); + + // Reload user + $u = User::newFromName( $username ); + $this->assertTrue( + (bool) $u->isBlockedFromCreateAccount(), + "Our sandbox user '$username' should NOT be able to create account" + ); + } + + function testCrappyCrossWikiBlocks() { + // Delete the last round's block if it's still there + $oldBlock = Block::newFromTarget( 'UserOnForeignWiki' ); + if ( $oldBlock ) { + // An old block will prevent our new one from saving. + $oldBlock->delete(); + } + + // Foreign perspective (blockee not on current wiki)... + $block = new Block( + /* $address */ 'UserOnForeignWiki', + /* $user */ 14146, + /* $by */ 0, + /* $reason */ 'crosswiki block...', + /* $timestamp */ wfTimestampNow(), + /* $auto */ false, + /* $expiry */ $this->db->getInfinity(), + /* anonOnly */ false, + /* $createAccount */ true, + /* $enableAutoblock */ true, + /* $hideName (ipb_deleted) */ true, + /* $blockEmail */ true, + /* $allowUsertalk */ false, + /* $byName */ 'MetaWikiUser' + ); + + $res = $block->insert( $this->db ); + $this->assertTrue( (bool)$res['id'], 'Block succeeded' ); + + // Local perspective (blockee on current wiki)... + $user = User::newFromName( 'UserOnForeignWiki' ); + $user->addToDatabase(); + // Set user ID to match the test value + $this->db->update( 'user', array( 'user_id' => 14146 ), array( 'user_id' => $user->getId() ) ); + $user = null; // clear + + $block = Block::newFromID( $res['id'] ); + $this->assertEquals( 'UserOnForeignWiki', $block->getTarget()->getName(), 'Correct blockee name' ); + $this->assertEquals( '14146', $block->getTarget()->getId(), 'Correct blockee id' ); + $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' ); + $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' ); + $this->assertEquals( 0, $block->getBy(), 'Correct blocker id' ); + } } diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php index 6c3e6664..b5418dd7 100644 --- a/tests/phpunit/includes/CdbTest.php +++ b/tests/phpunit/includes/CdbTest.php @@ -8,7 +8,7 @@ class CdbTest extends MediaWikiTestCase { public function setUp() { if ( !CdbReader::haveExtension() ) { - $this->markTestIncomplete( 'This test requires native CDB support to be present.' ); + $this->markTestSkipped( 'Native CDB support is not available' ); } } diff --git a/tests/phpunit/includes/DiffHistoryBlobTest.php b/tests/phpunit/includes/DiffHistoryBlobTest.php new file mode 100644 index 00000000..cdb6ed2f --- /dev/null +++ b/tests/phpunit/includes/DiffHistoryBlobTest.php @@ -0,0 +1,40 @@ +<?php + +class DiffHistoryBlobTest extends MediaWikiTestCase { + function setUp() { + if ( !extension_loaded( 'xdiff' ) ) { + $this->markTestSkipped( 'The xdiff extension is not available' ); + return; + } + if ( !function_exists( 'xdiff_string_rabdiff' ) ) { + $this->markTestSkipped( 'The version of xdiff extension is lower than 1.5.0' ); + return; + } + if ( !extension_loaded( 'hash' ) && !extension_loaded( 'mhash' ) ) { + $this->markTestSkipped( 'Neither the hash nor mhash extension is available' ); + return; + } + } + + /** + * Test for DiffHistoryBlob::xdiffAdler32() + * @dataProvider provideXdiffAdler32 + */ + function testXdiffAdler32( $input ) { + $xdiffHash = substr( xdiff_string_rabdiff( $input, '' ), 0, 4 ); + $dhb = new DiffHistoryBlob; + $myHash = $dhb->xdiffAdler32( $input ); + $this->assertSame( bin2hex( $xdiffHash ), bin2hex( $myHash ), + "Hash of " . addcslashes( $input, "\0..\37!@\@\177..\377" ) ); + } + + function provideXdiffAdler32() { + return array( + array( '', 'Empty string' ), + array( "\0", 'Null' ), + array( "\0\0\0", "Several nulls" ), + array( "Hello", "An ASCII string" ), + array( str_repeat( "x", 6000 ), "A string larger than xdiff's NMAX (5552)" ) + ); + } +} diff --git a/tests/phpunit/includes/EditPageTest.php b/tests/phpunit/includes/EditPageTest.php index e98e9707..8ecfd7e5 100644 --- a/tests/phpunit/includes/EditPageTest.php +++ b/tests/phpunit/includes/EditPageTest.php @@ -1,5 +1,8 @@ <?php +/** + * @group Editing + */ class EditPageTest extends MediaWikiTestCase { /** @@ -27,7 +30,11 @@ class EditPageTest extends MediaWikiTestCase { array( "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo", "Section" - ) + ), + array( + "== Section== \t\r\n followed by whitespace (bug 35051)", + 'Section', + ), ); } } diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php index a9088cb2..903a6d25 100644 --- a/tests/phpunit/includes/ExtraParserTest.php +++ b/tests/phpunit/includes/ExtraParserTest.php @@ -21,6 +21,8 @@ class ExtraParserTest extends MediaWikiTestCase { $this->options = new ParserOptions; $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) ); $this->parser = new Parser; + + MagicWord::clearCache(); } // Bug 8689 - Long numeric lines kill the parser @@ -146,7 +148,7 @@ class ExtraParserTest extends MediaWikiTestCase { */ function testTrackingCategory() { $title = Title::newFromText( __FUNCTION__ ); - $catName = wfMsgForContent( 'broken-file-category' ); + $catName = wfMessage( 'broken-file-category' )->inContentLanguage()->text(); $cat = Title::makeTitleSafe( NS_CATEGORY, $catName ); $expected = array( $cat->getDBkey() ); $parserOutput = $this->parser->parse( "[[file:nonexistent]]" , $title, $this->options ); diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php index 3cb42f12..9097d301 100644 --- a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php +++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -55,6 +55,12 @@ class GlobalTest extends MediaWikiTestCase { wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) ); } + function testExpandIRI() { + $this->assertEquals( + "https://te.wikibooks.org/wiki/ఉబుంటు_వాడుకరి_మార్గదర్శని", + wfExpandIRI( "https://te.wikibooks.org/wiki/%E0%B0%89%E0%B0%AC%E0%B1%81%E0%B0%82%E0%B0%9F%E0%B1%81_%E0%B0%B5%E0%B0%BE%E0%B0%A1%E0%B1%81%E0%B0%95%E0%B0%B0%E0%B0%BF_%E0%B0%AE%E0%B0%BE%E0%B0%B0%E0%B1%8D%E0%B0%97%E0%B0%A6%E0%B0%B0%E0%B1%8D%E0%B0%B6%E0%B0%A8%E0%B0%BF" ) ); + } + function testReadOnlyEmpty() { global $wgReadOnly; $wgReadOnly = null; @@ -305,7 +311,7 @@ class GlobalTest extends MediaWikiTestCase { function testDebugFunctionTest() { - global $wgDebugLogFile, $wgOut, $wgShowDebug, $wgDebugTimestamps; + global $wgDebugLogFile, $wgDebugTimestamps; $old_log_file = $wgDebugLogFile; $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' ); @@ -327,33 +333,7 @@ class GlobalTest extends MediaWikiTestCase { wfDebug( "\00305This has böth UTF and control chars\003" ); $this->assertEquals( " 05This has böth UTF and control chars ", file_get_contents( $wgDebugLogFile ) ); unlink( $wgDebugLogFile ); - - - - $old_wgOut = $wgOut; - $old_wgShowDebug = $wgShowDebug; - - $wgOut = new MockOutputPage; - - $wgShowDebug = true; - - $message = "\00305This has böth UTF and control chars\003"; - - wfDebug( $message ); - - if( $wgOut->message == "JAJA is a stupid error message. Anyway, here's your message: $message" ) { - $this->assertTrue( true, 'MockOutputPage called, set the proper message.' ); - } - else { - $this->assertTrue( false, 'MockOutputPage was not called.' ); - } - - $wgOut = $old_wgOut; - $wgShowDebug = $old_wgShowDebug; - unlink( $wgDebugLogFile ); - - - + wfDebugMem(); $this->assertGreaterThan( 5000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) ); unlink( $wgDebugLogFile ); @@ -616,13 +596,3 @@ class GlobalTest extends MediaWikiTestCase { /* TODO: many more! */ } - -class MockOutputPage { - - public $message; - - function debug( $message ) { - $this->message = "JAJA is a stupid error message. Anyway, here's your message: $message"; - } -} - diff --git a/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php new file mode 100644 index 00000000..4c4c4c04 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php @@ -0,0 +1,35 @@ +<?php + +class wfGetCaller extends MediaWikiTestCase { + + function testZero() { + $this->assertEquals( __METHOD__, wfGetCaller( 1 ) ); + } + + function callerOne() { + return wfGetCaller(); + } + + function testOne() { + $this->assertEquals( "wfGetCaller::testOne", self::callerOne() ); + } + + function intermediateFunction( $level = 2, $n = 0 ) { + if ( $n > 0 ) + return self::intermediateFunction( $level, $n - 1 ); + return wfGetCaller( $level ); + } + + function testTwo() { + $this->assertEquals( "wfGetCaller::testTwo", self::intermediateFunction() ); + } + + function testN() { + $this->assertEquals( "wfGetCaller::testN", self::intermediateFunction( 2, 0 ) ); + $this->assertEquals( "wfGetCaller::intermediateFunction", self::intermediateFunction( 1, 0 ) ); + + for ($i=0; $i < 10; $i++) + $this->assertEquals( "wfGetCaller::intermediateFunction", self::intermediateFunction( $i + 1, $i ) ); + } +} + diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php index 67b60d32..a18f7922 100644 --- a/tests/phpunit/includes/HtmlTest.php +++ b/tests/phpunit/includes/HtmlTest.php @@ -6,15 +6,18 @@ class HtmlTest extends MediaWikiTestCase { private static $oldContLang; private static $oldLanguageCode; private static $oldNamespaces; + private static $oldHTML5; public function setUp() { - global $wgLang, $wgContLang, $wgLanguageCode; - + global $wgLang, $wgContLang, $wgLanguageCode, $wgHTML5; + + // Save globals self::$oldLang = $wgLang; self::$oldContLang = $wgContLang; self::$oldNamespaces = $wgContLang->getNamespaces(); self::$oldLanguageCode = $wgLanguageCode; - + self::$oldHTML5 = $wgHTML5; + $wgLanguageCode = 'en'; $wgContLang = $wgLang = Language::factory( $wgLanguageCode ); @@ -36,18 +39,41 @@ class HtmlTest extends MediaWikiTestCase { 9 => 'MediaWiki_talk', 10 => 'Template', 11 => 'Template_talk', + 14 => 'Category', + 15 => 'Category_talk', 100 => 'Custom', 101 => 'Custom_talk', ) ); } - + public function tearDown() { - global $wgLang, $wgContLang, $wgLanguageCode; + global $wgLang, $wgContLang, $wgLanguageCode, $wgHTML5; + // Restore globals $wgContLang->setNamespaces( self::$oldNamespaces ); $wgLang = self::$oldLang; $wgContLang = self::$oldContLang; $wgLanguageCode = self::$oldLanguageCode; + $wgHTML5 = self::$oldHTML5; + } + + /** + * Wrapper to easily set $wgHTML5 = true. + * Original value will be restored after test completion. + * @todo Move to MediaWikiTestCase + */ + public function enableHTML5() { + global $wgHTML5; + $wgHTML5 = true; + } + /** + * Wrapper to easily set $wgHTML5 = false + * Original value will be restored after test completion. + * @todo Move to MediaWikiTestCase + */ + public function disableHTML5() { + global $wgHTML5; + $wgHTML5 = false; } public function testExpandAttributesSkipsNullAndFalse() { @@ -213,7 +239,7 @@ class HtmlTest extends MediaWikiTestCase { function testNamespaceSelector() { $this->assertEquals( - '<select id="namespace" name="namespace">' . "\n" . + '<select>' . "\n" . '<option value="0">(Main)</option>' . "\n" . '<option value="1">Talk</option>' . "\n" . '<option value="2">User</option>' . "\n" . @@ -226,12 +252,15 @@ class HtmlTest extends MediaWikiTestCase { '<option value="9">MediaWiki talk</option>' . "\n" . '<option value="10">Template</option>' . "\n" . '<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . '<option value="100">Custom</option>' . "\n" . '<option value="101">Custom talk</option>' . "\n" . '</select>', Html::namespaceSelector(), 'Basic namespace selector without custom options' ); + $this->assertEquals( '<label for="mw-test-namespace">Select a namespace:</label> ' . '<select id="mw-test-namespace" name="wpNamespace">' . "\n" . @@ -248,6 +277,8 @@ class HtmlTest extends MediaWikiTestCase { '<option value="9">MediaWiki talk</option>' . "\n" . '<option value="10">Template</option>' . "\n" . '<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . '<option value="100">Custom</option>' . "\n" . '<option value="101">Custom talk</option>' . "\n" . '</select>', @@ -257,77 +288,281 @@ class HtmlTest extends MediaWikiTestCase { ), 'Basic namespace selector with custom values' ); - } - function testNamespaceSelectorIdAndNameDefaultsAttributes() { - - $this->assertNsSelectorIdAndName( - 'namespace', 'namespace', - Html::namespaceSelector( array(), array( - # neither 'id' nor 'name' key given - )), - "Neither 'id' nor 'name' key given" + $this->assertEquals( + '<label>Select a namespace:</label> ' . +'<select>' . "\n" . +'<option value="0">(Main)</option>' . "\n" . +'<option value="1">Talk</option>' . "\n" . +'<option value="2">User</option>' . "\n" . +'<option value="3">User talk</option>' . "\n" . +'<option value="4">MyWiki</option>' . "\n" . +'<option value="5">MyWiki Talk</option>' . "\n" . +'<option value="6">File</option>' . "\n" . +'<option value="7">File talk</option>' . "\n" . +'<option value="8">MediaWiki</option>' . "\n" . +'<option value="9">MediaWiki talk</option>' . "\n" . +'<option value="10">Template</option>' . "\n" . +'<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . +'<option value="100">Custom</option>' . "\n" . +'<option value="101">Custom talk</option>' . "\n" . +'</select>', + Html::namespaceSelector( + array( 'label' => 'Select a namespace:' ) + ), + 'Basic namespace selector with a custom label but no id attribtue for the <select>' ); + } - $this->assertNsSelectorIdAndName( - 'namespace', 'select_name', - Html::namespaceSelector( array(), array( - 'name' => 'select_name', - # no 'id' key given - )), - "No 'id' key given, 'name' given" + function testCanFilterOutNamespaces() { + $this->assertEquals( +'<select>' . "\n" . +'<option value="2">User</option>' . "\n" . +'<option value="4">MyWiki</option>' . "\n" . +'<option value="5">MyWiki Talk</option>' . "\n" . +'<option value="6">File</option>' . "\n" . +'<option value="7">File talk</option>' . "\n" . +'<option value="8">MediaWiki</option>' . "\n" . +'<option value="9">MediaWiki talk</option>' . "\n" . +'<option value="10">Template</option>' . "\n" . +'<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . +'</select>', + Html::namespaceSelector( + array( 'exclude' => array( 0, 1, 3, 100, 101 ) ) + ), + 'Namespace selector namespace filtering.' ); + } - $this->assertNsSelectorIdAndName( - 'select_id', 'namespace', - Html::namespaceSelector( array(), array( - 'id' => 'select_id', - # no 'name' key given - )), - "'id' given, no 'name' key given" + function testCanDisableANamespaces() { + $this->assertEquals( +'<select>' . "\n" . +'<option disabled="" value="0">(Main)</option>' . "\n" . +'<option disabled="" value="1">Talk</option>' . "\n" . +'<option disabled="" value="2">User</option>' . "\n" . +'<option disabled="" value="3">User talk</option>' . "\n" . +'<option disabled="" value="4">MyWiki</option>' . "\n" . +'<option value="5">MyWiki Talk</option>' . "\n" . +'<option value="6">File</option>' . "\n" . +'<option value="7">File talk</option>' . "\n" . +'<option value="8">MediaWiki</option>' . "\n" . +'<option value="9">MediaWiki talk</option>' . "\n" . +'<option value="10">Template</option>' . "\n" . +'<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . +'<option value="100">Custom</option>' . "\n" . +'<option value="101">Custom talk</option>' . "\n" . +'</select>', + Html::namespaceSelector( array( + 'disable' => array( 0, 1, 2, 3, 4 ) + ) ), + 'Namespace selector namespace disabling' ); + } - $this->assertNsSelectorIdAndName( - 'select_id', 'select_name', - Html::namespaceSelector( array(), array( - 'id' => 'select_id', - 'name' => 'select_name', - )), - "Both 'id' and 'name' given" + /** + * @dataProvider providesHtml5InputTypes + */ + function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) { + $this->enableHTML5(); + $this->assertEquals( + '<input type="' . $HTML5InputType . '" />', + HTML::element( 'input', array( 'type' => $HTML5InputType ) ), + 'In HTML5, HTML::element() should accept type="' . $HTML5InputType . '"' ); } /** - * Helper to verify <select> attributes generated by Html::namespaceSelector() - * This helper expect the Html method to use 'namespace' as a default value for - * both 'id' and 'name' attributes. - * - * @param String $expectedId <select> id attribute value - * @param String $expectedName <select> name attribute value - * @param String $html Output of a call to Html::namespaceSelector() - * @param String $msg Optional message (default: '') - */ - function assertNsSelectorIdAndName( $expectedId, $expectedName, $html, $msg = '' ) { - $actualId = 'namespace'; - if( 1 === preg_match( '/id="(.+?)"/', $html, $m ) ) { - $actualId = $m[1]; + * List of input element types values introduced by HTML5 + * Full list at http://www.w3.org/TR/html-markup/input.html + */ + function providesHtml5InputTypes() { + $types = array( + 'datetime', + 'datetime-local', + 'date', + 'month', + 'time', + 'week', + 'number', + 'range', + 'email', + 'url', + 'search', + 'tel', + 'color', + ); + $cases = array(); + foreach( $types as $type ) { + $cases[] = array( $type ); } + return $cases; + } - $actualName = 'namespace'; - if( 1 === preg_match( '/name="(.+?)"/', $html, $m ) ) { - $actualName = $m[1]; - } - $this->assertEquals( - array( #expected - 'id' => $expectedId, - 'name' => $expectedName, - ), - array( #actual - 'id' => $actualId, - 'name' => $actualName, - ), - 'Html::namespaceSelector() got wrong id and/or name attribute(s). ' . $msg + /** + * Test out Html::element drops default value + * @cover Html::dropDefaults + * @dataProvider provideElementsWithAttributesHavingDefaultValues + */ + function testDropDefaults( $expected, $element, $message = '' ) { + $this->enableHTML5(); + $this->assertEquals( $expected, $element, $message ); + } + + function provideElementsWithAttributesHavingDefaultValues() { + # Use cases in a concise format: + # <expected>, <element name>, <array of attributes> [, <message>] + # Will be mapped to Html::element() + $cases = array(); + + ### Generic cases, match $attribDefault static array + $cases[] = array( '<area />', + 'area', array( 'shape' => 'rect' ) + ); + + $cases[] = array( '<button></button>', + 'button', array( 'formaction' => 'GET' ) + ); + $cases[] = array( '<button></button>', + 'button', array( 'formenctype' => 'application/x-www-form-urlencoded' ) + ); + $cases[] = array( '<button></button>', + 'button', array( 'type' => 'submit' ) + ); + + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'height' => '150' ) + ); + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'width' => '300' ) + ); + # Also check with numeric values + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'height' => 150 ) + ); + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'width' => 300 ) + ); + + $cases[] = array( '<command />', + 'command', array( 'type' => 'command' ) + ); + + $cases[] = array( '<form></form>', + 'form', array( 'action' => 'GET' ) + ); + $cases[] = array( '<form></form>', + 'form', array( 'autocomplete' => 'on' ) + ); + $cases[] = array( '<form></form>', + 'form', array( 'enctype' => 'application/x-www-form-urlencoded' ) + ); + + $cases[] = array( '<input />', + 'input', array( 'formaction' => 'GET' ) + ); + $cases[] = array( '<input />', + 'input', array( 'type' => 'text' ) + ); + + $cases[] = array( '<keygen />', + 'keygen', array( 'keytype' => 'rsa' ) ); + + $cases[] = array( '<link />', + 'link', array( 'media' => 'all' ) + ); + + $cases[] = array( '<menu></menu>', + 'menu', array( 'type' => 'list' ) + ); + + $cases[] = array( '<script></script>', + 'script', array( 'type' => 'text/javascript' ) + ); + + $cases[] = array( '<style></style>', + 'style', array( 'media' => 'all' ) + ); + $cases[] = array( '<style></style>', + 'style', array( 'type' => 'text/css' ) + ); + + $cases[] = array( '<textarea></textarea>', + 'textarea', array( 'wrap' => 'soft' ) + ); + + ### SPECIFIC CASES + + # <link type="text/css" /> + $cases[] = array( '<link />', + 'link', array( 'type' => 'text/css' ) + ); + + # <input /> specific handling + $cases[] = array( '<input type="checkbox" />', + 'input', array( 'type' => 'checkbox', 'value' => 'on' ), + 'Default value "on" is stripped of checkboxes', + ); + $cases[] = array( '<input type="radio" />', + 'input', array( 'type' => 'radio', 'value' => 'on' ), + 'Default value "on" is stripped of radio buttons', + ); + $cases[] = array( '<input type="submit" value="Submit" />', + 'input', array( 'type' => 'submit', 'value' => 'Submit' ), + 'Default value "Submit" is kept on submit buttons (for possible l10n issues)', + ); + $cases[] = array( '<input type="color" />', + 'input', array( 'type' => 'color', 'value' => '' ), + ); + $cases[] = array( '<input type="range" />', + 'input', array( 'type' => 'range', 'value' => '' ), + ); + + # <select /> specifc handling + $cases[] = array( '<select multiple=""></select>', + 'select', array( 'size' => '4', 'multiple' => true ), + ); + # .. with numeric value + $cases[] = array( '<select multiple=""></select>', + 'select', array( 'size' => 4, 'multiple' => true ), + ); + $cases[] = array( '<select></select>', + 'select', array( 'size' => '1', 'multiple' => false ), + ); + # .. with numeric value + $cases[] = array( '<select></select>', + 'select', array( 'size' => 1, 'multiple' => false ), + ); + + # Passing an array as value + $cases[] = array( '<a class="css-class-one css-class-two"></a>', + 'a', array( 'class' => array( 'css-class-one', 'css-class-two' ) ), + "dropDefaults accepts values given as an array" + ); + + # FIXME: doDropDefault should remove defaults given in an array + # Expected should be '<a></a>' + $cases[] = array( '<a class=""></a>', + 'a', array( 'class' => array( '', '' ) ), + "dropDefaults accepts values given as an array" + ); + + + # Craft the Html elements + $ret = array(); + foreach( $cases as $case ) { + $ret[] = array( + $case[0], + Html::element( $case[1], $case[2] ) + ); + } + return $ret; } } diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php index 4397b879..f50b2fe9 100644 --- a/tests/phpunit/includes/IPTest.php +++ b/tests/phpunit/includes/IPTest.php @@ -1,6 +1,7 @@ <?php /** * Tests for IP validity functions. Ported from /t/inc/IP.t by avar. + * @group IP */ class IPTest extends MediaWikiTestCase { @@ -14,9 +15,9 @@ class IPTest extends MediaWikiTestCase { $this->assertFalse( IP::isIPAddress( "" ), 'Empty string is not an IP' ); $this->assertFalse( IP::isIPAddress( 'abc' ), 'Garbage IP string' ); $this->assertFalse( IP::isIPAddress( ':' ), 'Single ":" is not an IP' ); - $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurence' ); - $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurence, last at end' ); - $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurence, firt at beginning' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurrence' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurrence, last at end' ); + $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurrence, firt at beginning' ); $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' ); $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' ); $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' ); @@ -505,4 +506,37 @@ class IPTest extends MediaWikiTestCase { array( '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ), ); } + + /** + * Test for IP::prettifyIP() + * @dataProvider provideIPsToPrettify + */ + function testPrettifyIP( $ip, $prettified ) { + $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" ); + } + + /** + * Provider for IP::testPrettifyIP() + */ + function provideIPsToPrettify() { + return array( + array( '0:0:0:0:0:0:0:0', '::' ), + array( '0:0:0::0:0:0', '::' ), + array( '0:0:0:1:0:0:0:0', '0:0:0:1::' ), + array( '0:0::f', '::f' ), + array( '0::0:0:0:33:fef:b', '::33:fef:b' ), + array( '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ), + array( '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ), + array( 'abbc:2004::0:0:0:0', 'abbc:2004::' ), + array( 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ), + array( '0:0:0:0:0:0:0:0/16', '::/16' ), + array( '0:0:0::0:0:0/64', '::/64' ), + array( '0:0::f/52', '::f/52' ), + array( '::0:0:33:fef:b/52', '::33:fef:b/52' ), + array( '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ), + array( '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ), + array( 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ), + array( 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ), + ); + } } diff --git a/tests/phpunit/includes/LinksUpdateTest.php b/tests/phpunit/includes/LinksUpdateTest.php new file mode 100644 index 00000000..49462001 --- /dev/null +++ b/tests/phpunit/includes/LinksUpdateTest.php @@ -0,0 +1,154 @@ +<?php + +/** + * + * @group Database + * ^--- make sure temporary tables are used. + */ +class LinksUpdateTest extends MediaWikiTestCase { + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge ( $this->tablesUsed, + array( 'interwiki', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + function setUp() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( 'interwiki', + array('iw_prefix'), + array( 'iw_prefix' => 'linksupdatetest', + 'iw_url' => 'http://testing.com/wiki/$1', + 'iw_api' => 'http://testing.com/w/api.php', + 'iw_local' => 0, + 'iw_trans' => 0, + 'iw_wikiid' => 'linksupdatetest', + ) ); + } + + protected function makeTitleAndParserOutput( $name, $id ) { + $t = Title::newFromText( $name ); + $t->mArticleID = $id; # XXX: this is fugly + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + + return array( $t, $po ); + } + + public function testUpdate_pagelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addLink( Title::newFromText( "Foo" ) ); + $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored + $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored + $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored + + $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array( + array( NS_MAIN, 'Foo' ), + ) ); + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + + $po->addLink( Title::newFromText( "Bar" ) ); + + $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array( + array( NS_MAIN, 'Bar' ), + ) ); + } + + public function testUpdate_externallinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addExternalLink( "http://testing.com/wiki/Foo" ); + + $this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array( + array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ), + ) ); + } + + public function testUpdate_categorylinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addCategory( "Foo", "FOO" ); + + $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array( + array( 'Foo', "FOO\nTESTING" ), + ) ); + } + + public function testUpdate_iwlinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' ); + $po->addInterwikiLink( $target ); + + $this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array( + array( 'linksupdatetest', 'Foo' ), + ) ); + } + + public function testUpdate_templatelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 ); + + $this->assertLinksUpdate( $t, $po, 'templatelinks', 'tl_namespace, tl_title', 'tl_from = 111', array( + array( NS_TEMPLATE, 'Foo' ), + ) ); + } + + public function testUpdate_imagelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addImage( "Foo.png" ); + + + $this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array( + array( 'Foo.png' ), + ) ); + } + + public function testUpdate_langlinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addLanguageLink( Title::newFromText( "en:Foo" ) ); + + + $this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array( + array( 'En', 'Foo' ), + ) ); + } + + public function testUpdate_page_props() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->setProperty( "foo", "bar" ); + + $this->assertLinksUpdate( $t, $po, 'page_props', 'pp_propname, pp_value', 'pp_page = 111', array( + array( 'foo', 'bar' ), + ) ); + } + + #@todo: test recursive, too! + + protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) { + $update = new LinksUpdate( $title, $parserOutput ); + + $update->doUpdate(); + + $this->assertSelect( $table, $fields, $condition, $expectedRows ); + } +} + diff --git a/tests/phpunit/includes/LocalisationCacheTest.php b/tests/phpunit/includes/LocalisationCacheTest.php new file mode 100644 index 00000000..356db87c --- /dev/null +++ b/tests/phpunit/includes/LocalisationCacheTest.php @@ -0,0 +1,31 @@ +<?php + +class LocalisationCacheTest extends MediaWikiTestCase { + public function testPuralRulesFallback() { + $cache = Language::getLocalisationCache(); + + $this->assertEquals( + $cache->getItem( 'ru', 'pluralRules' ), + $cache->getItem( 'os', 'pluralRules' ), + 'os plural rules (undefined) fallback to ru (defined)' + ); + + $this->assertEquals( + $cache->getItem( 'ru', 'compiledPluralRules' ), + $cache->getItem( 'os', 'compiledPluralRules' ), + 'os compiled plural rules (undefined) fallback to ru (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'pluralRules' ), + $cache->getItem( 'de', 'pluralRules' ), + 'ksh plural rules (defined) dont fallback to de (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'compiledPluralRules' ), + $cache->getItem( 'de', 'compiledPluralRules' ), + 'ksh compiled plural rules (defined) dont fallback to de (defined)' + ); + } +} diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php index 6b231fc5..3b05d675 100644 --- a/tests/phpunit/includes/MWNamespaceTest.php +++ b/tests/phpunit/includes/MWNamespaceTest.php @@ -53,10 +53,6 @@ class MWNamespaceTest extends MediaWikiTestCase { $this->assertIsNotSubject( NS_TALK ); $this->assertIsNotSubject( NS_USER_TALK ); $this->assertIsNotSubject( 101 ); # user defined - - // Back compat - $this->assertTrue( MWNamespace::isMain( NS_MAIN ) == MWNamespace::isSubject( NS_MAIN ) ); - $this->assertTrue( MWNamespace::isMain( NS_USER_TALK ) == MWNamespace::isSubject( NS_USER_TALK ) ); } /** @@ -442,6 +438,36 @@ class MWNamespaceTest extends MediaWikiTestCase { } /** + */ + public function testGetSubjectNamespaces() { + $subjectsNS = MWNamespace::getSubjectNamespaces(); + $this->assertContains( NS_MAIN, $subjectsNS, + "Talk namespaces should have NS_MAIN" ); + $this->assertNotContains( NS_TALK, $subjectsNS, + "Talk namespaces should have NS_TALK" ); + + $this->assertNotContains( NS_MEDIA, $subjectsNS, + "Talk namespaces should not have NS_MEDIA" ); + $this->assertNotContains( NS_SPECIAL, $subjectsNS, + "Talk namespaces should not have NS_SPECIAL" ); + } + + /** + */ + public function testGetTalkNamespaces() { + $talkNS = MWNamespace::getTalkNamespaces(); + $this->assertContains( NS_TALK, $talkNS, + "Subject namespaces should have NS_TALK" ); + $this->assertNotContains( NS_MAIN, $talkNS, + "Subject namespaces should not have NS_MAIN" ); + + $this->assertNotContains( NS_MEDIA, $talkNS, + "Subject namespaces should not have NS_MEDIA" ); + $this->assertNotContains( NS_SPECIAL, $talkNS, + "Subject namespaces should not have NS_SPECIAL" ); + } + + /** * Some namespaces are always capitalized per code definition * in MWNamespace::$alwaysCapitalizedNamespaces */ @@ -550,6 +576,15 @@ class MWNamespaceTest extends MediaWikiTestCase { } + public function testIsNonincludable() { + global $wgNonincludableNamespaces; + $wgNonincludableNamespaces = array( NS_USER ); + + $this->assertTrue( MWNamespace::isNonincludable( NS_USER ) ); + + $this->assertFalse( MWNamespace::isNonincludable( NS_TEMPLATE ) ); + } + ####### HELPERS ########################################################### function __call( $method, $args ) { // Call the real method if it exists diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index 295b6d74..20181fd4 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -16,6 +16,8 @@ class MessageTest extends MediaWikiLangTestCase { $this->assertInstanceOf( 'Message', wfMessage( 'i-dont-exist-evar' ) ); $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() ); $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->text() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->plain() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->escaped() ); } function testInLanguage() { diff --git a/tests/phpunit/includes/PreferencesTest.php b/tests/phpunit/includes/PreferencesTest.php new file mode 100644 index 00000000..0e123177 --- /dev/null +++ b/tests/phpunit/includes/PreferencesTest.php @@ -0,0 +1,75 @@ +<?php + +class PreferencesTest extends MediaWikiTestCase { + /** Array of User objects */ + private $prefUsers; + private $context; + + function __construct() { + parent::__construct(); + global $wgEnableEmail; + + $this->prefUsers['noemail'] = new User; + + $this->prefUsers['notauth'] = new User; + $this->prefUsers['notauth'] + ->setEmail( 'noauth@example.org' ); + + $this->prefUsers['auth'] = new User; + $this->prefUsers['auth'] + ->setEmail( 'noauth@example.org' ); + $this->prefUsers['auth'] + ->setEmailAuthenticationTimestamp( 1330946623 ); + + $this->context = new RequestContext; + $this->context->setTitle( Title::newFromText('PreferencesTest') ); + + //some tests depends on email setting + $wgEnableEmail = true; + } + + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + function testEmailFieldsWhenUserHasNoEmail() { + $prefs = $this->prefsFor( 'noemail' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-none', $prefs['emailaddress']['cssclass'] ); + } + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + function testEmailFieldsWhenUserEmailNotAuthenticated() { + $prefs = $this->prefsFor( 'notauth' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-not-authenticated', $prefs['emailaddress']['cssclass'] ); + } + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + function testEmailFieldsWhenUserEmailIsAuthenticated() { + $prefs = $this->prefsFor( 'auth' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-authenticated', $prefs['emailaddress']['cssclass'] ); + } + + /** Helper */ + function prefsFor( $user_key ) { + $preferences = array(); + Preferences::profilePreferences( + $this->prefUsers[$user_key] + , $this->context + , $preferences + ); + return $preferences; + } +} diff --git a/tests/phpunit/includes/RecentChangeTest.php b/tests/phpunit/includes/RecentChangeTest.php new file mode 100644 index 00000000..fbf271cc --- /dev/null +++ b/tests/phpunit/includes/RecentChangeTest.php @@ -0,0 +1,273 @@ +<?php +/** + * @group Database + */ +class RecentChangeTest extends MediaWikiTestCase { + protected $title; + protected $target; + protected $user; + protected $user_comment; + protected $context; + + function __construct() { + parent::__construct(); + + $this->title = Title::newFromText( 'SomeTitle' ); + $this->target = Title::newFromText( 'TestTarget' ); + $this->user = User::newFromName( 'UserName' ); + + $this->user_comment = '<User comment about action>'; + $this->context = RequestContext::newExtraneousContext( $this->title ); + } + + /** + * The testIrcMsgForAction* tests are supposed to cover the hacky + * LogFormatter::getIRCActionText / bug 34508 + * + * Third parties bots listen to those messages. They are clever enough + * to fetch the i18n messages from the wiki and then analyze the IRC feed + * to reverse engineer the $1, $2 messages. + * One thing bots can not detect is when MediaWiki change the meaning of + * a message like what happened when we deployed 1.19. $1 became the user + * performing the action which broke basically all bots around. + * + * Should cover the following log actions (which are most commonly used by bots): + * - block/block + * - block/unblock + * - delete/delete + * - delete/restore + * - newusers/create + * - newusers/create2 + * - newusers/autocreate + * - move/move + * - move/move_redir + * - protect/protect + * - protect/modifyprotect + * - protect/unprotect + * - upload/upload + * + * As well as the following Auto Edit Summaries: + * - blank + * - replace + * - rollback + * - undo + */ + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeBlock() { + # block/block + $this->assertIRCComment( + $this->context->msg( 'blocklogentry', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'block', 'block', + array(), + $this->user_comment + ); + # block/unblock + $this->assertIRCComment( + $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'block', 'unblock', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeDelete() { + # delete/delete + $this->assertIRCComment( + $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'delete', 'delete', + array(), + $this->user_comment + ); + + # delete/restore + $this->assertIRCComment( + $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'delete', 'restore', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeNewusers() { + $this->assertIRCComment( + 'New user account', + 'newusers', 'newusers', + array() + ); + $this->assertIRCComment( + 'New user account', + 'newusers', 'create', + array() + ); + $this->assertIRCComment( + 'created new account SomeTitle', + 'newusers', 'create2', + array() + ); + $this->assertIRCComment( + 'Account created automatically', + 'newusers', 'autocreate', + array() + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeMove() { + $move_params = array( + '4::target' => $this->target->getPrefixedText(), + '5::noredir' => 0, + ); + + # move/move + $this->assertIRCComment( + $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' )->plain() . ': ' . $this->user_comment, + 'move', 'move', + $move_params, + $this->user_comment + ); + + # move/move_redir + $this->assertIRCComment( + $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )->plain() . ': ' . $this->user_comment, + 'move', 'move_redir', + $move_params, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypePatrol() { + # patrol/patrol + $this->assertIRCComment( + $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(), + 'patrol', 'patrol', + array( + '4::curid' => '777', + '5::previd' => '666', + '6::auto' => 0, + ) + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeProtect() { + $protectParams = array( + '[edit=sysop] (indefinite) [move=sysop] (indefinite)' + ); + + # protect/protect + $this->assertIRCComment( + $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] )->plain() . ': ' . $this->user_comment, + 'protect', 'protect', + $protectParams, + $this->user_comment + ); + + # protect/unprotect + $this->assertIRCComment( + $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'protect', 'unprotect', + array(), + $this->user_comment + ); + + # protect/modify + $this->assertIRCComment( + $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] )->plain() . ': ' . $this->user_comment, + 'protect', 'modify', + $protectParams, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeUpload() { + # upload/upload + $this->assertIRCComment( + $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'upload', 'upload', + array(), + $this->user_comment + ); + + # upload/overwrite + $this->assertIRCComment( + $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'upload', 'overwrite', + array(), + $this->user_comment + ); + } + + /** + * @todo: Emulate these edits somehow and extract + * raw edit summary from RecentChange object + * -- + + function testIrcMsgForBlankingAES() { + // $this->context->msg( 'autosumm-blank', .. ); + } + + function testIrcMsgForReplaceAES() { + // $this->context->msg( 'autosumm-replace', .. ); + } + + function testIrcMsgForRollbackAES() { + // $this->context->msg( 'revertpage', .. ); + } + + function testIrcMsgForUndoAES() { + // $this->context->msg( 'undo-summary', .. ); + } + + * -- + */ + + /** + * @param $expected String Expected IRC text without colors codes + * @param $type String Log type (move, delete, suppress, patrol ...) + * @param $action String A log type action + * @param $comment String (optional) A comment for the log action + * @param $msg String (optional) A message for PHPUnit :-) + */ + function assertIRCComment( $expected, $type, $action, $params, $comment = null, $msg = '' ) { + + $logEntry = new ManualLogEntry( $type, $action ); + $logEntry->setPerformer( $this->user ); + $logEntry->setTarget( $this->title ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + $logEntry->setParameters( $params ); + + $formatter = LogFormatter::newFromEntry( $logEntry ); + $formatter->setContext( $this->context ); + + // Apply the same transformation as done in RecentChange::getIRCLine for rc_comment + $ircRcComment = RecentChange::cleanupForIRC( $formatter->getIRCActionComment() ); + + $this->assertEquals( + $expected, + $ircRcComment, + $msg + ); + } + +} diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php new file mode 100644 index 00000000..8a7facec --- /dev/null +++ b/tests/phpunit/includes/RevisionStorageTest.php @@ -0,0 +1,408 @@ +<?php + +/** + * Test class for Revision storage. + * + * @group Database + * ^--- important, causes temporary tables to be used instead of the real database + * + * @group medium + * ^--- important, causes tests not to fail with timeout + */ +class RevisionStorageTest extends MediaWikiTestCase { + + var $the_page; + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge( $this->tablesUsed, + array( 'page', + 'revision', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + public function setUp() { + if ( !$this->the_page ) { + $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" ); + } + } + + protected function makeRevision( $props = null ) { + if ( $props === null ) $props = array(); + + if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) $props['text'] = 'Lorem Ipsum'; + if ( !isset( $props['comment'] ) ) $props['comment'] = 'just a test'; + if ( !isset( $props['page'] ) ) $props['page'] = $this->the_page->getId(); + + $rev = new Revision( $props ); + + $dbw = wfgetDB( DB_MASTER ); + $rev->insertOn( $dbw ); + + return $rev; + } + + protected function createPage( $page, $text, $model = null ) { + if ( is_string( $page ) ) $page = Title::newFromText( $page ); + if ( $page instanceof Title ) $page = new WikiPage( $page ); + + if ( $page->exists() ) { + $page->doDeleteArticle( "done" ); + } + + $page->doEdit( $text, "testing", EDIT_NEW ); + + return $page; + } + + protected function assertRevEquals( Revision $orig, Revision $rev = null ) { + $this->assertNotNull( $rev, 'missing revision' ); + + $this->assertEquals( $orig->getId(), $rev->getId() ); + $this->assertEquals( $orig->getPage(), $rev->getPage() ); + $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() ); + $this->assertEquals( $orig->getUser(), $rev->getUser() ); + $this->assertEquals( $orig->getSha1(), $rev->getSha1() ); + } + + /** + * @covers Revision::__construct + */ + public function testConstructFromRow() + { + $orig = $this->makeRevision(); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = new Revision( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromRow + */ + public function testNewFromRow() + { + $orig = $this->makeRevision(); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + + /** + * @covers Revision::newFromArchiveRow + */ + public function testNewFromArchiveRow() + { + $page = $this->createPage( 'RevisionStorageTest_testNewFromArchiveRow', 'Lorem Ipsum' ); + $orig = $page->getRevision(); + $page->doDeleteArticle( 'test Revision::newFromArchiveRow' ); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'archive', '*', array( 'ar_rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromArchiveRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromId + */ + public function testNewFromId() + { + $orig = $this->makeRevision(); + + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::fetchRevision + */ + public function testFetchRevision() + { + $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one' ); + $id1 = $page->getRevision()->getId(); + + $page->doEdit( 'two', 'second rev' ); + $id2 = $page->getRevision()->getId(); + + $res = Revision::fetchRevision( $page->getTitle() ); + + #note: order is unspecified + $rows = array(); + while ( ( $row = $res->fetchObject() ) ) { + $rows[ $row->rev_id ]= $row; + } + + $row = $res->fetchObject(); + $this->assertEquals( 1, count($rows), 'expected exactly one revision' ); + $this->assertArrayHasKey( $id2, $rows, 'missing revision with id ' . $id2 ); + } + + /** + * @covers Revision::selectFields + */ + public function testSelectFields() + { + $fields = Revision::selectFields(); + + $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields'); + $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields'); + $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields'); + $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields'); + } + + /** + * @covers Revision::getPage + */ + public function testGetPage() + { + $page = $this->the_page; + + $orig = $this->makeRevision( array( 'page' => $page->getId() ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( $page->getId(), $rev->getPage() ); + } + + /** + * @covers Revision::getText + */ + public function testGetText() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello.', $rev->getText() ); + } + + /** + * @covers Revision::revText + */ + public function testRevText() + { + $this->hideDeprecated( 'Revision::revText' ); + $orig = $this->makeRevision( array( 'text' => 'hello hello rev.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello rev.', $rev->revText() ); + } + + /** + * @covers Revision::getRawText + */ + public function testGetRawText() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello raw.', $rev->getRawText() ); + } + /** + * @covers Revision::isCurrent + */ + public function testIsCurrent() + { + $page = $this->createPage( 'RevisionStorageTest_testIsCurrent', 'Lorem Ipsum' ); + $rev1 = $page->getRevision(); + + # @todo: find out if this should be true + # $this->assertTrue( $rev1->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertTrue( $rev1x->isCurrent() ); + + $page->doEdit( 'Bla bla', 'second rev' ); + $rev2 = $page->getRevision(); + + # @todo: find out if this should be true + # $this->assertTrue( $rev2->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertFalse( $rev1x->isCurrent() ); + + $rev2x = Revision::newFromId( $rev2->getId() ); + $this->assertTrue( $rev2x->isCurrent() ); + } + + /** + * @covers Revision::getPrevious + */ + public function testGetPrevious() + { + $page = $this->createPage( 'RevisionStorageTest_testGetPrevious', 'Lorem Ipsum testGetPrevious' ); + $rev1 = $page->getRevision(); + + $this->assertNull( $rev1->getPrevious() ); + + $page->doEdit( 'Bla bla', 'second rev testGetPrevious' ); + $rev2 = $page->getRevision(); + + $this->assertNotNull( $rev2->getPrevious() ); + $this->assertEquals( $rev1->getId(), $rev2->getPrevious()->getId() ); + } + + /** + * @covers Revision::getNext + */ + public function testGetNext() + { + $page = $this->createPage( 'RevisionStorageTest_testGetNext', 'Lorem Ipsum testGetNext' ); + $rev1 = $page->getRevision(); + + $this->assertNull( $rev1->getNext() ); + + $page->doEdit( 'Bla bla', 'second rev testGetNext' ); + $rev2 = $page->getRevision(); + + $this->assertNotNull( $rev1->getNext() ); + $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() ); + } + + /** + * @covers Revision::newNullRevision + */ + public function testNewNullRevision() + { + $page = $this->createPage( 'RevisionStorageTest_testNewNullRevision', 'some testing text' ); + $orig = $page->getRevision(); + + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false ); + + $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' ); + $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' ); + $this->assertEquals( 'some testing text', $rev->getText() ); + } + + public function dataUserWasLastToEdit() { + return array( + array( #0 + 3, true, # actually the last edit + ), + array( #1 + 2, true, # not the current edit, but still by this user + ), + array( #2 + 1, false, # edit by another user + ), + array( #3 + 0, false, # first edit, by this user, but another user edited in the mean time + ), + ); + } + + /** + * @dataProvider dataUserWasLastToEdit + */ + public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) { + $userA = \User::newFromName( "RevisionStorageTest_userA" ); + $userB = \User::newFromName( "RevisionStorageTest_userB" ); + + if ( $userA->getId() === 0 ) { + $userA = \User::createNew( $userA->getName() ); + } + + if ( $userB->getId() === 0 ) { + $userB = \User::createNew( $userB->getName() ); + } + + $dbw = wfGetDB( DB_MASTER ); + $revisions = array(); + + // create revisions ----------------------------- + $page = WikiPage::factory( Title::newFromText( 'RevisionStorageTest_testUserWasLastToEdit' ) ); + + # zero + $revisions[0] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000000', + 'user' => $userA->getId(), + 'text' => 'zero', + 'summary' => 'edit zero' + ) ); + $revisions[0]->insertOn( $dbw ); + + # one + $revisions[1] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000100', + 'user' => $userA->getId(), + 'text' => 'one', + 'summary' => 'edit one' + ) ); + $revisions[1]->insertOn( $dbw ); + + # two + $revisions[2] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000200', + 'user' => $userB->getId(), + 'text' => 'two', + 'summary' => 'edit two' + ) ); + $revisions[2]->insertOn( $dbw ); + + # three + $revisions[3] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000300', + 'user' => $userA->getId(), + 'text' => 'three', + 'summary' => 'edit three' + ) ); + $revisions[3]->insertOn( $dbw ); + + # four + $revisions[4] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000200', + 'user' => $userA->getId(), + 'text' => 'zero', + 'summary' => 'edit four' + ) ); + $revisions[4]->insertOn( $dbw ); + + // test it --------------------------------- + $since = $revisions[ $sinceIdx ]->getTimestamp(); + + $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since ); + + $this->assertEquals( $expectedLast, $wasLast ); + } +} diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php index 77a371d5..59ba0a04 100644 --- a/tests/phpunit/includes/SampleTest.php +++ b/tests/phpunit/includes/SampleTest.php @@ -47,7 +47,7 @@ class TestSample extends MediaWikiLangTestCase { array( 'Text', null, 'Text' ), array( 'text', null, 'Text' ), array( 'Text', NS_USER, 'User:Text' ), - array( 'Photo.jpg', NS_IMAGE, 'File:Photo.jpg' ) + array( 'Photo.jpg', NS_FILE, 'File:Photo.jpg' ) ); } diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php index b76aa5c7..66af2581 100644 --- a/tests/phpunit/includes/SanitizerTest.php +++ b/tests/phpunit/includes/SanitizerTest.php @@ -110,21 +110,27 @@ class SanitizerTest extends MediaWikiTestCase { $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=&foobar;' ), array( 'foo' => '&foobar;' ), 'Entity-like items are accepted' ); } - function testDeprecatedAttributes() { - $GLOBALS['wgCleanupPresentationalAttributes'] = true; - $this->assertEquals( Sanitizer::fixTagAttributes( 'clear="left"', 'br' ), ' style="clear: left;"', 'Deprecated attributes are converted to styles when enabled.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'clear="all"', 'br' ), ' style="clear: both;"', 'clear=all is converted to clear: both; not clear: all;' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'CLEAR="ALL"', 'br' ), ' style="clear: both;"', 'clear=ALL is not treated differently from clear=all' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'width="100"', 'td' ), ' style="width: 100px;"', 'Numeric sizes use pixels instead of numbers.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'width="100%"', 'td' ), ' style="width: 100%;"', 'Units are allowed in sizes.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'WIDTH="100%"', 'td' ), ' style="width: 100%;"', 'Uppercase WIDTH is treated as lowercase width.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'WiDTh="100%"', 'td' ), ' style="width: 100%;"', 'Mixed case does not break WiDTh.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'nowrap="true"', 'td' ), ' style="white-space: nowrap;"', 'nowrap attribute is output as white-space: nowrap; not something else.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'nowrap=""', 'td' ), ' style="white-space: nowrap;"', 'nowrap="" is considered true, not false' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'NOWRAP="true"', 'td' ), ' style="white-space: nowrap;"', 'nowrap attribute works when uppercase.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'NoWrAp="true"', 'td' ), ' style="white-space: nowrap;"', 'nowrap attribute works when mixed-case.' ); - $GLOBALS['wgCleanupPresentationalAttributes'] = false; - $this->assertEquals( Sanitizer::fixTagAttributes( 'clear="left"', 'br' ), ' clear="left"', 'Deprecated attributes are not converted to styles when enabled.' ); + /** + * @dataProvider provideDeprecatedAttributes + */ + function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl ) { + $this->assertEquals( " $inputAttr", Sanitizer::fixTagAttributes( $inputAttr, $inputEl ) ); + } + + public static function provideDeprecatedAttributes() { + return array( + array( 'clear="left"', 'br' ), + array( 'clear="all"', 'br' ), + array( 'width="100"', 'td' ), + array( 'nowrap="true"', 'td' ), + array( 'nowrap=""', 'td' ), + array( 'align="right"', 'td' ), + array( 'align="center"', 'table' ), + array( 'align="left"', 'tr' ), + array( 'align="center"', 'div' ), + array( 'align="left"', 'h1' ), + array( 'align="left"', 'span' ), + ); } /** diff --git a/tests/phpunit/includes/TemplateCategoriesTest.php b/tests/phpunit/includes/TemplateCategoriesTest.php index de9d6dc6..39ce6e31 100644 --- a/tests/phpunit/includes/TemplateCategoriesTest.php +++ b/tests/phpunit/includes/TemplateCategoriesTest.php @@ -3,26 +3,24 @@ /** * @group Database */ -require dirname( __FILE__ ) . "/../../../maintenance/runJobs.php"; +require __DIR__ . "/../../../maintenance/runJobs.php"; class TemplateCategoriesTest extends MediaWikiLangTestCase { function testTemplateCategories() { - global $wgUser; - $title = Title::newFromText( "Categorized from template" ); - $article = new Article( $title ); - $wgUser = new User(); - $wgUser->mRights['*'] = array( 'createpage', 'edit', 'purge' ); + $page = WikiPage::factory( $title ); + $user = new User(); + $user->mRights = array( 'createpage', 'edit', 'purge' ); - $status = $article->doEdit( '{{Categorising template}}', 'Create a page with a template', 0 ); + $status = $page->doEdit( '{{Categorising template}}', 'Create a page with a template', 0, false, $user ); $this->assertEquals( array() , $title->getParentCategories() ); - $template = new Article( Title::newFromText( 'Template:Categorising template' ) ); - $status = $template->doEdit( '[[Category:Solved bugs]]', 'Add a category through a template', 0 ); + $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) ); + $status = $template->doEdit( '[[Category:Solved bugs]]', 'Add a category through a template', 0, false, $user ); // Run the job queue $jobs = new RunJobs; diff --git a/tests/phpunit/includes/api/ApiTestUser.php b/tests/phpunit/includes/TestUser.php index 8d5f61a7..c4d89455 100644 --- a/tests/phpunit/includes/api/ApiTestUser.php +++ b/tests/phpunit/includes/TestUser.php @@ -1,7 +1,7 @@ <?php /* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */ -class ApiTestUser { +class TestUser { public $username; public $password; public $email; @@ -55,5 +55,4 @@ class ApiTestUser { $this->user->saveSettings(); } - } diff --git a/tests/phpunit/includes/TimestampTest.php b/tests/phpunit/includes/TimestampTest.php new file mode 100644 index 00000000..231228f5 --- /dev/null +++ b/tests/phpunit/includes/TimestampTest.php @@ -0,0 +1,72 @@ +<?php + +/** + * Tests timestamp parsing and output. + */ +class TimestampTest extends MediaWikiTestCase { + /** + * Test parsing of valid timestamps and outputing to MW format. + * @dataProvider provideValidTimestamps + */ + function testValidParse( $format, $original, $expected ) { + $timestamp = new MWTimestamp( $original ); + $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) ); + } + + /** + * Test outputting valid timestamps to different formats. + * @dataProvider provideValidTimestamps + */ + function testValidOutput( $format, $expected, $original ) { + $timestamp = new MWTimestamp( $original ); + $this->assertEquals( $expected, (string) $timestamp->getTimestamp( $format ) ); + } + + /** + * Test an invalid timestamp. + * @expectedException TimestampException + */ + function testInvalidParse() { + $timestamp = new MWTimestamp( "This is not a timestamp." ); + } + + /** + * Test requesting an invalid output format. + * @expectedException TimestampException + */ + function testInvalidOutput() { + $timestamp = new MWTimestamp( '1343761268' ); + $timestamp->getTimestamp( 98 ); + } + + /** + * Test human readable timestamp format. + */ + function testHumanOutput() { + $timestamp = new MWTimestamp( time() - 3600 ); + $this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->toString() ); + } + + /** + * Returns a list of valid timestamps in the format: + * array( type, timestamp_of_type, timestamp_in_MW ) + */ + function provideValidTimestamps() { + return array( + // Various formats + array( TS_UNIX, '1343761268', '20120731190108' ), + array( TS_MW, '20120731190108', '20120731190108' ), + array( TS_DB, '2012-07-31 19:01:08', '20120731190108' ), + array( TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ), + array( TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ), + array( TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ), + array( TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ), + array( TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ), + array( TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ), + array( TS_DB2, '2012-07-31 19:01:08', '20120731190108' ), + // Some extremes and weird values + array( TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ), + array( TS_UNIX, '-62135596801', '00001231235959' ) + ); + } +} diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php index 2f1103e8..aed658ba 100644 --- a/tests/phpunit/includes/TitleMethodsTest.php +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -21,8 +21,8 @@ class TitleMethodsTest extends MediaWikiTestCase { $titleA = Title::newFromText( $titleA ); $titleB = Title::newFromText( $titleB ); - $this->assertEquals( $titleA->equals( $titleB ), $expectedBool ); - $this->assertEquals( $titleB->equals( $titleA ), $expectedBool ); + $this->assertEquals( $expectedBool, $titleA->equals( $titleB ) ); + $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) ); } public function dataInNamespace() { @@ -43,7 +43,7 @@ class TitleMethodsTest extends MediaWikiTestCase { */ public function testInNamespace( $title, $ns, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $title->inNamespace( $ns ), $expectedBool ); + $this->assertEquals( $expectedBool, $title->inNamespace( $ns ) ); } public function testInNamespaces() { @@ -72,7 +72,130 @@ class TitleMethodsTest extends MediaWikiTestCase { */ public function testHasSubjectNamespace( $title, $ns, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $title->hasSubjectNamespace( $ns ), $expectedBool ); + $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) ); + } + + public function dataIsCssOrJsPage() { + return array( + array( 'Foo', false ), + array( 'Foo.js', false ), + array( 'Foo/bar.js', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', false ), + array( 'User talk:Foo/bar.css', false ), + array( 'User:Foo/bar.js.xxx', false ), + array( 'User:Foo/bar.xxx', false ), + array( 'MediaWiki:Foo.js', true ), + array( 'MediaWiki:Foo.css', true ), + array( 'MediaWiki:Foo.JS', false ), + array( 'MediaWiki:Foo.CSS', false ), + array( 'MediaWiki:Foo.css.xxx', false ), + ); + } + + /** + * @dataProvider dataIsCssOrJsPage + */ + public function testIsCssOrJsPage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssOrJsPage() ); + } + + + public function dataIsCssJsSubpage() { + return array( + array( 'Foo', false ), + array( 'Foo.js', false ), + array( 'Foo/bar.js', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo/bar.js', true ), + array( 'User:Foo/bar.css', true ), + array( 'User talk:Foo/bar.css', false ), + array( 'User:Foo/bar.js.xxx', false ), + array( 'User:Foo/bar.xxx', false ), + array( 'MediaWiki:Foo.js', false ), + array( 'User:Foo/bar.JS', false ), + array( 'User:Foo/bar.CSS', false ), + ); + } + + /** + * @dataProvider dataIsCssJsSubpage + */ + public function testIsCssJsSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssJsSubpage() ); + } + + public function dataIsCssSubpage() { + return array( + array( 'Foo', false ), + array( 'Foo.css', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo.css', false ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', true ), + ); + } + + /** + * @dataProvider dataIsCssSubpage + */ + public function testIsCssSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssSubpage() ); + } + + public function dataIsJsSubpage() { + return array( + array( 'Foo', false ), + array( 'Foo.css', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo.css', false ), + array( 'User:Foo/bar.js', true ), + array( 'User:Foo/bar.css', false ), + ); + } + + /** + * @dataProvider dataIsJsSubpage + */ + public function testIsJsSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isJsSubpage() ); + } + + public function dataIsWikitextPage() { + return array( + array( 'Foo', true ), + array( 'Foo.js', true ), + array( 'Foo/bar.js', true ), + array( 'User:Foo', true ), + array( 'User:Foo.js', true ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', false ), + array( 'User talk:Foo/bar.css', true ), + array( 'User:Foo/bar.js.xxx', true ), + array( 'User:Foo/bar.xxx', true ), + array( 'MediaWiki:Foo.js', false ), + array( 'MediaWiki:Foo.css', false ), + array( 'MediaWiki:Foo/bar.css', false ), + array( 'User:Foo/bar.JS', true ), + array( 'User:Foo/bar.CSS', true ), + ); + } + + /** + * @dataProvider dataIsWikitextPage + */ + public function testIsWikitextPage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isWikitextPage() ); } } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index 1c8be5f9..f61652df 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -77,4 +77,79 @@ class TitleTest extends MediaWikiTestCase { } + /** + * @dataProvider provideCasesForGetpageviewlanguage + */ + function testGetpageviewlanguage( $expected, $titleText, $contLang, $lang, $variant, $msg='' ) { + // Save globals + global $wgContLang, $wgLang, $wgAllowUserJs, $wgLanguageCode, $wgDefaultLanguageVariant; + $save['wgContLang'] = $wgContLang; + $save['wgLang'] = $wgLang; + $save['wgAllowUserJs'] = $wgAllowUserJs; + $save['wgLanguageCode'] = $wgLanguageCode; + $save['wgDefaultLanguageVariant'] = $wgDefaultLanguageVariant; + + // Setup test environnement: + $wgContLang = Language::factory( $contLang ); + $wgLang = Language::factory( $lang ); + # To test out .js titles: + $wgAllowUserJs = true; + $wgLanguageCode = $contLang; + $wgDefaultLanguageVariant = $variant; + + $title = Title::newFromText( $titleText ); + $this->assertInstanceOf( 'Title', $title, + "Test must be passed a valid title text, you gave '$titleText'" + ); + $this->assertEquals( $expected, + $title->getPageViewLanguage()->getCode(), + $msg + ); + + // Restore globals + $wgContLang = $save['wgContLang']; + $wgLang = $save['wgLang']; + $wgAllowUserJs = $save['wgAllowUserJs']; + $wgLanguageCode = $save['wgLanguageCode']; + $wgDefaultLanguageVariant = $save['wgDefaultLanguageVariant']; + } + + function provideCasesForGetpageviewlanguage() { + # Format: + # - expected + # - Title name + # - wgContLang (expected in most case) + # - wgLang (on some specific pages) + # - wgDefaultLanguageVariant + # - Optional message + return array( + array( 'fr', 'Main_page', 'fr', 'fr', false ), + array( 'es', 'Main_page', 'es', 'zh-tw', false ), + array( 'zh', 'Main_page', 'zh', 'zh-tw', false ), + + array( 'es', 'Main_page', 'es', 'zh-tw', 'zh-cn' ), + array( 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ), + array( 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ), + array( 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ), + + array( 'zh-cn', 'Main_page', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ), + array( 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ), + + array( 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ), + array( 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ), + + ); + } } diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php index ef03e835..7a424aef 100644 --- a/tests/phpunit/includes/UserTest.php +++ b/tests/phpunit/includes/UserTest.php @@ -140,4 +140,32 @@ class UserTest extends MediaWikiTestCase { array( 'Ab cd', false, ' Ideographic space' ), ); } + + /** + * Test, if for all rights a right- message exist, + * which is used on Special:ListGroupRights as help text + * Extensions and core + */ + public function testAllRightsWithMessage() { + //Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights + $allRights = User::getAllRights(); + $allMessageKeys = Language::getMessageKeysFor( 'en' ); + + $rightsWithMessage = array(); + foreach ( $allMessageKeys as $message ) { + // === 0: must be at beginning of string (position 0) + if ( strpos( $message, 'right-' ) === 0 ) { + $rightsWithMessage[] = substr( $message, strlen( 'right-' ) ); + } + } + + sort( $allRights ); + sort( $rightsWithMessage ); + + $this->assertEquals( + $allRights, + $rightsWithMessage, + 'Each user rights (core/extensions) has a corresponding right- message.' + ); + } } diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php index e72408f6..1fc0b4b3 100644 --- a/tests/phpunit/includes/WebRequestTest.php +++ b/tests/phpunit/includes/WebRequestTest.php @@ -1,14 +1,22 @@ <?php class WebRequestTest extends MediaWikiTestCase { + static $oldServer; + + function setUp() { + self::$oldServer = $_SERVER; + } + + function tearDown() { + $_SERVER = self::$oldServer; + } + /** * @dataProvider provideDetectServer */ function testDetectServer( $expected, $input, $description ) { - $oldServer = $_SERVER; $_SERVER = $input; $result = WebRequest::detectServer(); - $_SERVER = $oldServer; $this->assertEquals( $expected, $result, $description ); } @@ -91,13 +99,11 @@ class WebRequestTest extends MediaWikiTestCase { */ function testGetIP( $expected, $input, $squid, $private, $description ) { global $wgSquidServersNoPurge, $wgUsePrivateIPs; - $oldServer = $_SERVER; $_SERVER = $input; $wgSquidServersNoPurge = $squid; $wgUsePrivateIPs = $private; $request = new WebRequest(); $result = $request->getIP(); - $_SERVER = $oldServer; $this->assertEquals( $expected, $result, $description ); } @@ -182,4 +188,29 @@ class WebRequestTest extends MediaWikiTestCase { # Next call throw an exception about lacking an IP $request->getIP(); } + + function languageProvider() { + return array( + array( '', array(), 'Empty Accept-Language header' ), + array( 'en', array( 'en' => 1 ), 'One language' ), + array( 'en, ar', array( 'en' => 1, 'ar' => 1 ), 'Two languages listed in appearance order.' ), + array( 'zh-cn,zh-tw', array( 'zh-cn' => 1, 'zh-tw' => 1 ), 'Two equally prefered languages, listed in appearance order per rfc3282. Checks c9119' ), + array( 'es, en; q=0.5', array( 'es' => 1, 'en' => '0.5' ), 'Spanish as first language and English and second' ), + array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Less prefered language first' ), + array( 'fr, en; q=0.5, es', array( 'fr' => 1, 'es' => 1, 'en' => '0.5' ), 'Three languages' ), + array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Two languages' ), + array( 'en, zh;q=0', array( 'en' => 1 ), "It's Chinese to me" ), + array( 'es; q=1, pt;q=0.7, it; q=0.6, de; q=0.1, ru;q=0', array( 'es' => '1', 'pt' => '0.7', 'it' => '0.6', 'de' => '0.1' ), 'Preference for romance languages' ), + array( 'en-gb, en-us; q=1', array( 'en-gb' => 1, 'en-us' => '1' ), 'Two equally prefered English variants' ), + ); + } + + /** + * @dataProvider languageProvider + */ + function testAcceptLang($acceptLanguageHeader, $expectedLanguages, $description) { + $_SERVER = array( 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader ); + $request = new WebRequest(); + $this->assertSame( $request->getAcceptLang(), $expectedLanguages, $description); + } } diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php new file mode 100644 index 00000000..0e1e1ce8 --- /dev/null +++ b/tests/phpunit/includes/WikiPageTest.php @@ -0,0 +1,784 @@ +<?php +/** +* @group Database +* ^--- important, causes temporary tables to be used instead of the real database +* @group medium +**/ + +class WikiPageTest extends MediaWikiLangTestCase { + + var $pages_to_delete; + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge ( $this->tablesUsed, + array( 'page', + 'revision', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + public function setUp() { + parent::setUp(); + $this->pages_to_delete = array(); + } + + public function tearDown() { + foreach ( $this->pages_to_delete as $p ) { + /* @var $p WikiPage */ + + try { + if ( $p->exists() ) { + $p->doDeleteArticle( "testing done." ); + } + } catch ( MWException $ex ) { + // fail silently + } + } + parent::tearDown(); + } + + protected function newPage( $title ) { + if ( is_string( $title ) ) $title = Title::newFromText( $title ); + + $p = new WikiPage( $title ); + + $this->pages_to_delete[] = $p; + + return $p; + } + + protected function createPage( $page, $text, $model = null ) { + if ( is_string( $page ) ) $page = Title::newFromText( $page ); + if ( $page instanceof Title ) $page = $this->newPage( $page ); + + $page->doEdit( $text, "testing", EDIT_NEW ); + + return $page; + } + + public function testDoEdit() { + $title = Title::newFromText( "WikiPageTest_testDoEdit" ); + + $page = $this->newPage( $title ); + + $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat."; + + $page->doEdit( $text, "testing 1" ); + + $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" ); + $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" ); + + $id = $page->getId(); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getText(); + $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' ); + + # ------------------------ + $text = "At vero eos et accusam et justo duo [[dolores]] et ea rebum. " + . "Stet clita kasd [[gubergren]], no sea takimata sanctus est."; + + $page->doEdit( $text, "testing 2" ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getText(); + $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); + } + + public function testDoQuickEdit() { + global $wgUser; + + $page = $this->createPage( "WikiPageTest_testDoQuickEdit", "original text" ); + + $text = "quick text"; + $page->doQuickEdit( $text, $wgUser, "testing q" ); + + # --------------------- + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $text, $page->getText() ); + } + + public function testDoDeleteArticle() { + $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" ); + $id = $page->getId(); + + $page->doDeleteArticle( "testing deletion" ); + + $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" ); + $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" ); + + $t = Title::newFromText( $page->getTitle()->getPrefixedText() ); + $this->assertFalse( $t->exists(), "Title::exists should return false after page was deleted" ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' ); + } + + public function testDoDeleteUpdates() { + $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" ); + $id = $page->getId(); + + $page->doDeleteUpdates( $id ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' ); + } + + public function testGetRevision() { + $page = $this->newPage( "WikiPageTest_testGetRevision" ); + + $rev = $page->getRevision(); + $this->assertNull( $rev ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $rev = $page->getRevision(); + + $this->assertEquals( $page->getLatest(), $rev->getId() ); + $this->assertEquals( "some text", $rev->getText() ); + } + + public function testGetText() { + $page = $this->newPage( "WikiPageTest_testGetText" ); + + $text = $page->getText(); + $this->assertFalse( $text ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $text = $page->getText(); + $this->assertEquals( "some text", $text ); + } + + public function testGetRawText() { + $page = $this->newPage( "WikiPageTest_testGetRawText" ); + + $text = $page->getRawText(); + $this->assertFalse( $text ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $text = $page->getRawText(); + $this->assertEquals( "some text", $text ); + } + + + public function testExists() { + $page = $this->newPage( "WikiPageTest_testExists" ); + $this->assertFalse( $page->exists() ); + + # ----------------- + $this->createPage( $page, "some text" ); + $this->assertTrue( $page->exists() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $page->exists() ); + + # ----------------- + $page->doDeleteArticle( "done testing" ); + $this->assertFalse( $page->exists() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertFalse( $page->exists() ); + } + + public function dataHasViewableContent() { + return array( + array( 'WikiPageTest_testHasViewableContent', false, true ), + array( 'Special:WikiPageTest_testHasViewableContent', false ), + array( 'MediaWiki:WikiPageTest_testHasViewableContent', false ), + array( 'Special:Userlogin', true ), + array( 'MediaWiki:help', true ), + ); + } + + /** + * @dataProvider dataHasViewableContent + */ + public function testHasViewableContent( $title, $viewable, $create = false ) { + $page = $this->newPage( $title ); + $this->assertEquals( $viewable, $page->hasViewableContent() ); + + if ( $create ) { + $this->createPage( $page, "some text" ); + $this->assertTrue( $page->hasViewableContent() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $page->hasViewableContent() ); + } + } + + public function dataGetRedirectTarget() { + return array( + array( 'WikiPageTest_testGetRedirectTarget_1', "hello world", null ), + array( 'WikiPageTest_testGetRedirectTarget_2', "#REDIRECT [[hello world]]", "Hello world" ), + ); + } + + /** + * @dataProvider dataGetRedirectTarget + */ + public function testGetRedirectTarget( $title, $text, $target ) { + $page = $this->createPage( $title, $text ); + + # now, test the actual redirect + $t = $page->getRedirectTarget(); + $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() ); + } + + /** + * @dataProvider dataGetRedirectTarget + */ + public function testIsRedirect( $title, $text, $target ) { + $page = $this->createPage( $title, $text ); + $this->assertEquals( !is_null( $target ), $page->isRedirect() ); + } + + public function dataIsCountable() { + return array( + + // any + array( 'WikiPageTest_testIsCountable', + '', + 'any', + true + ), + array( 'WikiPageTest_testIsCountable', + 'Foo', + 'any', + true + ), + + // comma + array( 'WikiPageTest_testIsCountable', + 'Foo', + 'comma', + false + ), + array( 'WikiPageTest_testIsCountable', + 'Foo, bar', + 'comma', + true + ), + + // link + array( 'WikiPageTest_testIsCountable', + 'Foo', + 'link', + false + ), + array( 'WikiPageTest_testIsCountable', + 'Foo [[bar]]', + 'link', + true + ), + + // redirects + array( 'WikiPageTest_testIsCountable', + '#REDIRECT [[bar]]', + 'any', + false + ), + array( 'WikiPageTest_testIsCountable', + '#REDIRECT [[bar]]', + 'comma', + false + ), + array( 'WikiPageTest_testIsCountable', + '#REDIRECT [[bar]]', + 'link', + false + ), + + // not a content namespace + array( 'Talk:WikiPageTest_testIsCountable', + 'Foo', + 'any', + false + ), + array( 'Talk:WikiPageTest_testIsCountable', + 'Foo, bar', + 'comma', + false + ), + array( 'Talk:WikiPageTest_testIsCountable', + 'Foo [[bar]]', + 'link', + false + ), + + // not a content namespace, different model + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + 'Foo', + 'any', + false + ), + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + 'Foo, bar', + 'comma', + false + ), + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + 'Foo [[bar]]', + 'link', + false + ), + ); + } + + + /** + * @dataProvider dataIsCountable + */ + public function testIsCountable( $title, $text, $mode, $expected ) { + global $wgArticleCountMethod; + + $old = $wgArticleCountMethod; + $wgArticleCountMethod = $mode; + + $page = $this->createPage( $title, $text ); + $editInfo = $page->prepareTextForEdit( $page->getText() ); + + $v = $page->isCountable(); + $w = $page->isCountable( $editInfo ); + $wgArticleCountMethod = $old; + + $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + + $this->assertEquals( $expected, $w, "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + } + + public function dataGetParserOutput() { + return array( + array("hello ''world''\n", "<p>hello <i>world</i></p>"), + // @todo: more...? + ); + } + + /** + * @dataProvider dataGetParserOutput + */ + public function testGetParserOutput( $text, $expectedHtml ) { + $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text ); + + $opt = new ParserOptions(); + $po = $page->getParserOutput( $opt ); + $text = $po->getText(); + + $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments + $text = preg_replace( '!\s*(</p>)!sm', '\1', $text ); # don't let tidy confuse us + + $this->assertEquals( $expectedHtml, $text ); + return $po; + } + + static $sections = + + "Intro + +== stuff == +hello world + +== test == +just a test + +== foo == +more stuff +"; + + + public function dataReplaceSection() { + return array( + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) ) + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "", + "No more", + null, + "No more" + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikiPageTest::$sections ) ) + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "8", + "No more", + null, + trim( WikiPageTest::$sections ) + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "new", + "No more", + "New", + trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more" + ), + ); + } + + /** + * @dataProvider dataReplaceSection + */ + public function testReplaceSection( $title, $text, $section, $with, $sectionTitle, $expected ) { + $page = $this->createPage( $title, $text ); + $text = $page->replaceSection( $section, $with, $sectionTitle ); + $text = trim( $text ); + + $this->assertEquals( $expected, $text ); + } + + /* @todo FIXME: fix this! + public function testGetUndoText() { + global $wgDiff3; + + wfSuppressWarnings(); + $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); + wfRestoreWarnings(); + + if( !$haveDiff3 ) { + $this->markTestSkipped( "diff3 not installed or not found" ); + return; + } + + $text = "one"; + $page = $this->createPage( "WikiPageTest_testGetUndoText", $text ); + $rev1 = $page->getRevision(); + + $text .= "\n\ntwo"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two"); + $rev2 = $page->getRevision(); + + $text .= "\n\nthree"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three"); + $rev3 = $page->getRevision(); + + $text .= "\n\nfour"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section four"); + $rev4 = $page->getRevision(); + + $text .= "\n\nfive"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section five"); + $rev5 = $page->getRevision(); + + $text .= "\n\nsix"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section six"); + $rev6 = $page->getRevision(); + + $undo6 = $page->getUndoText( $rev6 ); + if ( $undo6 === false ) $this->fail( "getUndoText failed for rev6" ); + $this->assertEquals( "one\n\ntwo\n\nthree\n\nfour\n\nfive", $undo6 ); + + $undo3 = $page->getUndoText( $rev4, $rev2 ); + if ( $undo3 === false ) $this->fail( "getUndoText failed for rev4..rev2" ); + $this->assertEquals( "one\n\ntwo\n\nfive", $undo3 ); + + $undo2 = $page->getUndoText( $rev2 ); + if ( $undo2 === false ) $this->fail( "getUndoText failed for rev2" ); + $this->assertEquals( "one\n\nfive", $undo2 ); + } + */ + + /** + * @todo FIXME: this is a better rollback test than the one below, but it keeps failing in jenkins for some reason. + */ + public function broken_testDoRollback() { + $admin = new User(); + $admin->setName("Admin"); + + $text = "one"; + $page = $this->newPage( "WikiPageTest_testDoRollback" ); + $page->doEdit( $text, "section one", EDIT_NEW, false, $admin ); + + $user1 = new User(); + $user1->setName( "127.0.1.11" ); + $text .= "\n\ntwo"; + $page = new WikiPage( $page->getTitle() ); + $page->doEdit( $text, "adding section two", 0, false, $user1 ); + + $user2 = new User(); + $user2->setName( "127.0.2.13" ); + $text .= "\n\nthree"; + $page = new WikiPage( $page->getTitle() ); + $page->doEdit( $text, "adding section three", 0, false, $user2 ); + + # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing + # or not committed under some circumstances. so, make sure the last revision has the right user name. + $dbr = wfGetDB( DB_SLAVE ); + $this->assertEquals( 3, Revision::countByPageId( $dbr, $page->getId() ) ); + + $page = new WikiPage( $page->getTitle() ); + $rev3 = $page->getRevision(); + $this->assertEquals( '127.0.2.13', $rev3->getUserText() ); + + $rev2 = $rev3->getPrevious(); + $this->assertEquals( '127.0.1.11', $rev2->getUserText() ); + + $rev1 = $rev2->getPrevious(); + $this->assertEquals( 'Admin', $rev1->getUserText() ); + + # now, try the actual rollback + $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... + $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user2->getName() ), null ); + $errors = $page->doRollback( $user2->getName(), "testing revert", $token, false, $details, $admin ); + + if ( $errors ) { + $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) ); + } + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); + $this->assertEquals( "one\n\ntwo", $page->getText() ); + } + + /** + * @todo FIXME: the above rollback test is better, but it keeps failing in jenkins for some reason. + */ + public function testDoRollback() { + $admin = new User(); + $admin->setName("Admin"); + + $text = "one"; + $page = $this->newPage( "WikiPageTest_testDoRollback" ); + $page->doEdit( $text, "section one", EDIT_NEW, false, $admin ); + $rev1 = $page->getRevision(); + + $user1 = new User(); + $user1->setName( "127.0.1.11" ); + $text .= "\n\ntwo"; + $page = new WikiPage( $page->getTitle() ); + $page->doEdit( $text, "adding section two", 0, false, $user1 ); + + # now, try the rollback + $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... + $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user1->getName() ), null ); + $errors = $page->doRollback( $user1->getName(), "testing revert", $token, false, $details, $admin ); + + if ( $errors ) { + $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) ); + } + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); + $this->assertEquals( "one", $page->getText() ); + } + + public function dataGetAutosummary( ) { + return array( + array( + 'Hello there, world!', + '#REDIRECT [[Foo]]', + 0, + '/^Redirected page .*Foo/' + ), + + array( + null, + 'Hello world!', + EDIT_NEW, + '/^Created page .*Hello/' + ), + + array( + 'Hello there, world!', + '', + 0, + '/^Blanked/' + ), + + array( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut + labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et + ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Hello world!', + 0, + '/^Replaced .*Hello/' + ), + + array( + 'foo', + 'bar', + 0, + '/^$/' + ), + ); + } + + /** + * @dataProvider dataGetAutoSummary + */ + public function testGetAutosummary( $old, $new, $flags, $expected ) { + $page = $this->newPage( "WikiPageTest_testGetAutosummary" ); + + $summary = $page->getAutosummary( $old, $new, $flags ); + + $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" ); + } + + public function dataGetAutoDeleteReason( ) { + return array( + array( + array(), + false, + false + ), + + array( + array( + array( "first edit", null ), + ), + "/first edit.*only contributor/", + false + ), + + array( + array( + array( "first edit", null ), + array( "second edit", null ), + ), + "/second edit.*only contributor/", + true + ), + + array( + array( + array( "first edit", "127.0.2.22" ), + array( "second edit", "127.0.3.33" ), + ), + "/second edit/", + true + ), + + array( + array( + array( "first edit: " + . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. " + . "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea " + . "takimata sanctus est Lorem ipsum dolor sit amet.'", null ), + ), + '/first edit:.*\.\.\."/', + false + ), + + array( + array( + array( "first edit", "127.0.2.22" ), + array( "", "127.0.3.33" ), + ), + "/before blanking.*first edit/", + true + ), + + ); + } + + /** + * @dataProvider dataGetAutoDeleteReason + */ + public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) { + global $wgUser; + + $page = $this->newPage( "WikiPageTest_testGetAutoDeleteReason" ); + + $c = 1; + + foreach ( $edits as $edit ) { + $user = new User(); + + if ( !empty( $edit[1] ) ) $user->setName( $edit[1] ); + else $user = $wgUser; + + $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user ); + + $c += 1; + } + + $reason = $page->getAutoDeleteReason( $hasHistory ); + + if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) $this->assertEquals( $expectedResult, $reason ); + else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), "Autosummary didn't match expected pattern $expectedResult: $reason" ); + + $this->assertEquals( $expectedHistory, $hasHistory, "expected \$hasHistory to be " . var_export( $expectedHistory, true ) ); + + $page->doDeleteArticle( "done" ); + } + + public function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ), + ); + } + + /** + * @dataProvider dataPreSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + $this->hideDeprecated( 'WikiPage::preSaveTransform' ); + $user = new User(); + $user->setName("127.0.0.1"); + + $page = $this->newPage( "WikiPageTest_testPreloadTransform" ); + $text = $page->preSaveTransform( $text, $user ); + + $this->assertEquals( $expected, $text ); + } + +} + diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php index 1d9361f2..93ed3dc7 100644 --- a/tests/phpunit/includes/XmlTest.php +++ b/tests/phpunit/includes/XmlTest.php @@ -193,52 +193,6 @@ class XmlTest extends MediaWikiTestCase { ); } - function testNamespaceSelector() { - $this->assertEquals( - '<select class="namespaceselector" id="namespace" name="namespace">' . "\n" . -'<option value="0">(Main)</option>' . "\n" . -'<option value="1">Talk</option>' . "\n" . -'<option value="2">User</option>' . "\n" . -'<option value="3">User talk</option>' . "\n" . -'<option value="4">MyWiki</option>' . "\n" . -'<option value="5">MyWiki Talk</option>' . "\n" . -'<option value="6">File</option>' . "\n" . -'<option value="7">File talk</option>' . "\n" . -'<option value="8">MediaWiki</option>' . "\n" . -'<option value="9">MediaWiki talk</option>' . "\n" . -'<option value="10">Template</option>' . "\n" . -'<option value="11">Template talk</option>' . "\n" . -'<option value="100">Custom</option>' . "\n" . -'<option value="101">Custom talk</option>' . "\n" . -'</select>', - Xml::namespaceSelector(), - 'Basic namespace selector without custom options' - ); - $this->assertEquals( - '<label for="namespace">Select a namespace:</label>' . -' <select class="namespaceselector" id="namespace" name="myname">' . "\n" . -'<option value="all">all</option>' . "\n" . -'<option value="0">(Main)</option>' . "\n" . -'<option value="1">Talk</option>' . "\n" . -'<option value="2" selected="">User</option>' . "\n" . -'<option value="3">User talk</option>' . "\n" . -'<option value="4">MyWiki</option>' . "\n" . -'<option value="5">MyWiki Talk</option>' . "\n" . -'<option value="6">File</option>' . "\n" . -'<option value="7">File talk</option>' . "\n" . -'<option value="8">MediaWiki</option>' . "\n" . -'<option value="9">MediaWiki talk</option>' . "\n" . -'<option value="10">Template</option>' . "\n" . -'<option value="11">Template talk</option>' . "\n" . -'<option value="100">Custom</option>' . "\n" . -'<option value="101">Custom talk</option>' . "\n" . -'</select>', - Xml::namespaceSelector( $selected = '2', $all = 'all', $element_name = 'myname', $label = 'Select a namespace:' ), - 'Basic namespace selector with custom values' - ); - } - - # # textarea # @@ -297,6 +251,15 @@ class XmlTest extends MediaWikiTestCase { ); } + function testLanguageSelector() { + $select = Xml::languageSelector( 'en', true, null, + array( 'id' => 'testlang' ), wfMessage( 'yourlanguage' ) ); + $this->assertEquals( + '<label for="testlang">Language:</label>', + $select[0] + ); + } + # # JS # diff --git a/tests/phpunit/includes/ZipDirectoryReaderTest.php b/tests/phpunit/includes/ZipDirectoryReaderTest.php index f7ca59e2..d90a6950 100644 --- a/tests/phpunit/includes/ZipDirectoryReaderTest.php +++ b/tests/phpunit/includes/ZipDirectoryReaderTest.php @@ -4,7 +4,7 @@ class ZipDirectoryReaderTest extends MediaWikiTestCase { var $zipDir, $entries; function setUp() { - $this->zipDir = dirname( __FILE__ ) . '/../data/zip'; + $this->zipDir = __DIR__ . '/../data/zip'; } function zipCallback( $entry ) { diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php index b95d8214..5dfceee8 100644 --- a/tests/phpunit/includes/api/ApiBlockTest.php +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiBlockTest extends ApiTestCase { @@ -32,8 +33,6 @@ class ApiBlockTest extends ApiTestCase { * Root cause is https://gerrit.wikimedia.org/r/3434 * Which made the Block/Unblock API to actually verify the token * previously always considered valid (bug 34212). - * - * @group Broken */ function testMakeNormalBlock() { @@ -57,7 +56,7 @@ class ApiBlockTest extends ApiTestCase { 'action' => 'block', 'user' => 'UTApiBlockee', 'reason' => 'Some reason', - 'token' => $pageinfo['blocktoken'] ), $data, false, self::$users['sysop']->user ); + 'token' => $pageinfo['blocktoken'] ), null, false, self::$users['sysop']->user ); $block = Block::newFromTarget('UTApiBlockee'); @@ -69,4 +68,50 @@ class ApiBlockTest extends ApiTestCase { } + /** + * @dataProvider provideBlockUnblockAction + */ + function testGetTokenUsingABlockingAction( $action ) { + $data = $this->doApiRequest( + array( + 'action' => $action, + 'user' => 'UTApiBlockee', + 'gettoken' => '' ), + null, + false, + self::$users['sysop']->user + ); + $this->assertEquals( 34, strlen( $data[0][$action]["{$action}token"] ) ); + } + + /** + * Attempting to block without a token should give a UsageException with + * error message: + * "The token parameter must be set" + * + * @dataProvider provideBlockUnblockAction + * @expectedException UsageException + */ + function testBlockingActionWithNoToken( $action ) { + $this->doApiRequest( + array( + 'action' => $action, + 'user' => 'UTApiBlockee', + 'reason' => 'Some reason', + ), + null, + false, + self::$users['sysop']->user + ); + } + + /** + * Just provide the 'block' and 'unblock' action to test both API calls + */ + function provideBlockUnblockAction() { + return array( + array( 'block' ), + array( 'unblock' ), + ); + } } diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php new file mode 100644 index 00000000..5297d6da --- /dev/null +++ b/tests/phpunit/includes/api/ApiEditPageTest.php @@ -0,0 +1,84 @@ +<?php + +/** + * Tests for MediaWiki api.php?action=edit. + * + * @author Daniel Kinzler + * + * @group API + * @group Database + */ +class ApiEditPageTest extends ApiTestCase { + + function setUp() { + parent::setUp(); + $this->doLogin(); + } + + function testEdit( ) { + $name = 'ApiEditPageTest_testEdit'; + + // -- test new page -------------------------------------------- + $apiResult = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', ) ); + $apiResult = $apiResult[0]; + + # Validate API result data + $this->assertArrayHasKey( 'edit', $apiResult ); + $this->assertArrayHasKey( 'result', $apiResult['edit'] ); + $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + + $this->assertArrayHasKey( 'new', $apiResult['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); + + $this->assertArrayHasKey( 'pageid', $apiResult['edit'] ); + + // -- test existing page, no change ---------------------------- + $data = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', ) ); + + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); + $this->assertArrayHasKey( 'nochange', $data[0]['edit'] ); + + // -- test existing page, with change -------------------------- + $data = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'different text' ) ); + + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] ); + + $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] ); + $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] ); + $this->assertNotEquals( + $data[0]['edit']['newrevid'], + $data[0]['edit']['oldrevid'], + "revision id should change after edit" + ); + } + + function testEditAppend() { + $this->markTestIncomplete( "not yet implemented" ); + } + + function testEditSection() { + $this->markTestIncomplete( "not yet implemented" ); + } + + function testUndo() { + $this->markTestIncomplete( "not yet implemented" ); + } + + function testEditNonText() { + $this->markTestIncomplete( "not yet implemented" ); + } +} diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php new file mode 100644 index 00000000..5243fca1 --- /dev/null +++ b/tests/phpunit/includes/api/ApiOptionsTest.php @@ -0,0 +1,276 @@ +<?php + +/** + * @group API + * @group Database + */ +class ApiOptionsTest extends MediaWikiLangTestCase { + + private $mTested, $mApiMainMock, $mUserMock, $mContext, $mSession; + + private $mOldGetPreferencesHooks = false; + + private static $Success = array( 'options' => 'success' ); + + function setUp() { + parent::setUp(); + + $this->mUserMock = $this->getMockBuilder( 'User' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->mApiMainMock = $this->getMockBuilder( 'ApiBase' ) + ->disableOriginalConstructor() + ->getMock(); + + // Set up groups + $this->mUserMock->expects( $this->any() ) + ->method( 'getEffectiveGroups' )->will( $this->returnValue( array( '*', 'user')) ); + + // Create a new context + $this->mContext = new DerivativeContext( new RequestContext() ); + $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) ); + $this->mContext->setUser( $this->mUserMock ); + + $this->mApiMainMock->expects( $this->any() ) + ->method( 'getContext' ) + ->will( $this->returnValue( $this->mContext ) ); + + $this->mApiMainMock->expects( $this->any() ) + ->method( 'getResult' ) + ->will( $this->returnValue( new ApiResult( $this->mApiMainMock ) ) ); + + + // Empty session + $this->mSession = array(); + + $this->mTested = new ApiOptions( $this->mApiMainMock, 'options' ); + + global $wgHooks; + if ( !isset( $wgHooks['GetPreferences'] ) ) { + $wgHooks['GetPreferences'] = array(); + } + $this->mOldGetPreferencesHooks = $wgHooks['GetPreferences']; + $wgHooks['GetPreferences'][] = array( $this, 'hookGetPreferences' ); + } + + public function tearDown() { + global $wgHooks; + + if ( $this->mOldGetPreferencesHooks !== false ) { + $wgHooks['GetPreferences'] = $this->mOldGetPreferencesHooks; + $this->mOldGetPreferencesHooks = false; + } + + parent::tearDown(); + } + + public function hookGetPreferences( $user, &$preferences ) { + foreach ( array( 'name', 'willBeNull', 'willBeEmpty', 'willBeHappy' ) as $k ) { + $preferences[$k] = array( + 'type' => 'text', + 'section' => 'test', + 'label' => ' ', + ); + } + + return true; + } + + private function getSampleRequest( $custom = array() ) { + $request = array( + 'token' => '123ABC', + 'change' => null, + 'optionname' => null, + 'optionvalue' => null, + ); + return array_merge( $request, $custom ); + } + + private function executeQuery( $request ) { + $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) ); + $this->mTested->execute(); + return $this->mTested->getResult()->getData(); + } + + /** + * @expectedException UsageException + */ + public function testNoToken() { + $request = $this->getSampleRequest( array( 'token' => null ) ); + + $this->executeQuery( $request ); + } + + public function testAnon() { + $this->mUserMock->expects( $this->once() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( true ) ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'notloggedin', $e->getCodeString() ); + $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoOptionname() { + try { + $request = $this->getSampleRequest( array( 'optionvalue' => '1' ) ); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nooptionname', $e->getCodeString() ); + $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoChanges() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'saveSettings' ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nochanges', $e->getCodeString() ); + $this->assertEquals( 'No changes were requested', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testReset() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'reset' => '' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionWithValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name', 'optionvalue' => 'value' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionResetValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( null ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name' ) ); + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testChange() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 1 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeNull' ), $this->equalTo( null ) ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 6 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testResetChangeOption() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $args = array( + 'reset' => '', + 'change' => 'willBeHappy=Happy', + 'optionname' => 'name', + 'optionvalue' => 'value' + ); + + $response = $this->executeQuery( $this->getSampleRequest( $args ) ); + + $this->assertEquals( self::$Success, $response ); + } +} diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php index 70c20746..2566c6cd 100644 --- a/tests/phpunit/includes/api/ApiPurgeTest.php +++ b/tests/phpunit/includes/api/ApiPurgeTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiPurgeTest extends ApiTestCase { diff --git a/tests/phpunit/includes/api/ApiQueryTest.php b/tests/phpunit/includes/api/ApiQueryTest.php index ae05a30a..a4b9dc70 100644 --- a/tests/phpunit/includes/api/ApiQueryTest.php +++ b/tests/phpunit/includes/api/ApiQueryTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiQueryTest extends ApiTestCase { diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php index 1d9c3238..c3eacd5b 100644 --- a/tests/phpunit/includes/api/ApiTest.php +++ b/tests/phpunit/includes/api/ApiTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiTest extends ApiTestCase { diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 8801391f..b84292e3 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -1,10 +1,6 @@ <?php abstract class ApiTestCase extends MediaWikiLangTestCase { - /** - * @var Array of ApiTestUser - */ - public static $users; protected static $apiUrl; /** @@ -23,13 +19,13 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { $wgRequest = new FauxRequest( array() ); self::$users = array( - 'sysop' => new ApiTestUser( + 'sysop' => new TestUser( 'Apitestsysop', 'Api Test Sysop', 'api_test_sysop@example.com', array( 'sysop' ) ), - 'uploader' => new ApiTestUser( + 'uploader' => new TestUser( 'Apitestuser', 'Api Test User', 'api_test_user@example.com', @@ -43,15 +39,31 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { } - protected function doApiRequest( $params, $session = null, $appendModule = false, $user = null ) { + protected function doApiRequest( Array $params, Array $session = null, $appendModule = false, User $user = null ) { + global $wgRequest, $wgUser; + if ( is_null( $session ) ) { - $session = array(); + # re-use existing global session by default + $session = $wgRequest->getSessionArray(); } - $context = $this->apiContext->newTestContext( $params, $session, $user ); + # set up global environment + if ( $user ) { + $wgUser = $user; + } + + $wgRequest = new FauxRequest( $params, true, $session ); + RequestContext::getMain()->setRequest( $wgRequest ); + + # set up local environment + $context = $this->apiContext->newTestContext( $wgRequest, $wgUser ); + $module = new ApiMain( $context, true ); + + # run it! $module->execute(); + # construct result $results = array( $module->getResultData(), $context->getRequest(), @@ -68,11 +80,17 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { * Add an edit token to the API request * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the * request, without actually requesting a "real" edit token - * @param $params: key-value API params - * @param $session: session array - * @param $user String|null A User object for the context + * @param $params Array: key-value API params + * @param $session Array|null: session array + * @param $user User|null A User object for the context */ - protected function doApiRequestWithToken( $params, $session, $user = null ) { + protected function doApiRequestWithToken( Array $params, Array $session = null, User $user = null ) { + global $wgRequest; + + if ( $session === null ) { + $session = $wgRequest->getSessionArray(); + } + if ( $session['wsToken'] ) { // add edit token to fake session $session['wsEditToken'] = $session['wsToken']; @@ -97,17 +115,17 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { 'lgtoken' => $token, 'lgname' => self::$users['sysop']->username, 'lgpassword' => self::$users['sysop']->password - ), $data ); + ), $data[2] ); return $data; } - protected function getTokenList( $user ) { + protected function getTokenList( $user, $session = null ) { $data = $this->doApiRequest( array( 'action' => 'query', 'titles' => 'Main Page', - 'intoken' => 'edit|delete|protect|move|block|unblock', - 'prop' => 'info' ), false, $user->user ); + 'intoken' => 'edit|delete|protect|move|block|unblock|watch', + 'prop' => 'info' ), $session, false, $user->user ); return $data; } } @@ -154,14 +172,13 @@ class ApiTestContext extends RequestContext { /** * Returns a DerivativeContext with the request variables in place * - * @param $params Array key-value API params - * @param $session Array session data + * @param $request WebRequest request object including parameters and session * @param $user User or null * @return DerivativeContext */ - public function newTestContext( $params, $session, $user = null ) { + public function newTestContext( WebRequest $request, User $user = null ) { $context = new DerivativeContext( $this ); - $context->setRequest( new FauxRequest( $params, true, $session ) ); + $context->setRequest( $request ); if ( $user !== null ) { $context->setUser( $user ); } diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php index 7a700326..642fed05 100644 --- a/tests/phpunit/includes/api/ApiUploadTest.php +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php index b7803746..d2e98152 100644 --- a/tests/phpunit/includes/api/ApiWatchTest.php +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -1,8 +1,9 @@ <?php /** + * @group API * @group Database - * @todo This test suite is severly broken and need a full review + * @todo This test suite is severly broken and need a full review */ class ApiWatchTest extends ApiTestCase { @@ -10,28 +11,28 @@ class ApiWatchTest extends ApiTestCase { parent::setUp(); $this->doLogin(); } - + function getTokens() { - return $this->getTokenList( self::$users['sysop'] ); + $data = $this->getTokenList( self::$users['sysop'] ); + + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + return $pageinfo; } /** - * @group Broken */ function testWatchEdit() { - - $data = $this->getTokens(); - - $keys = array_keys( $data[0]['query']['pages'] ); - $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]; + $pageinfo = $this->getTokens(); $data = $this->doApiRequest( array( 'action' => 'edit', 'title' => 'UTPage', 'text' => 'new text', 'token' => $pageinfo['edittoken'], - 'watchlist' => 'watch' ), $data ); + 'watchlist' => 'watch' ) ); $this->assertArrayHasKey( 'edit', $data[0] ); $this->assertArrayHasKey( 'result', $data[0]['edit'] ); $this->assertEquals( 'Success', $data[0]['edit']['result'] ); @@ -41,13 +42,14 @@ class ApiWatchTest extends ApiTestCase { /** * @depends testWatchEdit - * @group Broken */ function testWatchClear() { - + + $pageinfo = $this->getTokens(); + $data = $this->doApiRequest( array( 'action' => 'query', - 'list' => 'watchlist' ), $data ); + 'list' => 'watchlist' ) ); if ( isset( $data[0]['query']['watchlist'] ) ) { $wl = $data[0]['query']['watchlist']; @@ -56,7 +58,8 @@ class ApiWatchTest extends ApiTestCase { $data = $this->doApiRequest( array( 'action' => 'watch', 'title' => $page['title'], - 'unwatch' => true ), $data ); + 'unwatch' => true, + 'token' => $pageinfo['watchtoken'] ) ); } } $data = $this->doApiRequest( array( @@ -70,22 +73,17 @@ class ApiWatchTest extends ApiTestCase { } /** - * @group Broken - */ + */ function testWatchProtect() { - - $data = $this->getTokens(); - - $keys = array_keys( $data[0]['query']['pages'] ); - $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]; + + $pageinfo = $this->getTokens(); $data = $this->doApiRequest( array( 'action' => 'protect', 'token' => $pageinfo['protecttoken'], 'title' => 'UTPage', 'protections' => 'edit=sysop', - 'watchlist' => 'unwatch' ), $data ); + 'watchlist' => 'unwatch' ) ); $this->assertArrayHasKey( 'protect', $data[0] ); $this->assertArrayHasKey( 'protections', $data[0]['protect'] ); @@ -94,21 +92,20 @@ class ApiWatchTest extends ApiTestCase { } /** - * @group Broken */ function testGetRollbackToken() { - - $data = $this->getTokens(); - + + $pageinfo = $this->getTokens(); + if ( !Title::newFromText( 'UTPage' )->exists() ) { - $this->markTestIncomplete( "The article [[UTPage]] does not exist" ); + $this->markTestSkipped( "The article [[UTPage]] does not exist" ); //TODO: just create it? } $data = $this->doApiRequest( array( 'action' => 'query', 'prop' => 'revisions', 'titles' => 'UTPage', - 'rvtoken' => 'rollback' ), $data ); + 'rvtoken' => 'rollback' ) ); $this->assertArrayHasKey( 'query', $data[0] ); $this->assertArrayHasKey( 'pages', $data[0]['query'] ); @@ -116,7 +113,7 @@ class ApiWatchTest extends ApiTestCase { $key = array_pop( $keys ); if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) { - $this->markTestIncomplete( "Target page (UTPage) doesn't exist" ); + $this->markTestSkipped( "Target page (UTPage) doesn't exist" ); } $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] ); @@ -128,21 +125,27 @@ class ApiWatchTest extends ApiTestCase { } /** - * @depends testGetRollbackToken * @group Broken + * Broken because there is currently no revision info in the $pageinfo + * + * @depends testGetRollbackToken */ function testWatchRollback( $data ) { $keys = array_keys( $data[0]['query']['pages'] ); $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]['revisions'][0]; + $pageinfo = $data[0]['query']['pages'][$key]; + $revinfo = $pageinfo['revisions'][0]; try { $data = $this->doApiRequest( array( 'action' => 'rollback', 'title' => 'UTPage', - 'user' => $pageinfo['user'], + 'user' => $revinfo['user'], 'token' => $pageinfo['rollbacktoken'], - 'watchlist' => 'watch' ), $data ); + 'watchlist' => 'watch' ) ); + + $this->assertArrayHasKey( 'rollback', $data[0] ); + $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); } catch( UsageException $ue ) { if( $ue->getCodeString() == 'onlyauthor' ) { $this->markTestIncomplete( "Only one author to 'UTPage', cannot test rollback" ); @@ -150,32 +153,23 @@ class ApiWatchTest extends ApiTestCase { $this->fail( "Received error '" . $ue->getCodeString() . "'" ); } } - - $this->assertArrayHasKey( 'rollback', $data[0] ); - $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); } /** - * @group Broken */ function testWatchDelete() { - - $data = $this->getTokens(); - - $keys = array_keys( $data[0]['query']['pages'] ); - $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]; + $pageinfo = $this->getTokens(); $data = $this->doApiRequest( array( 'action' => 'delete', 'token' => $pageinfo['deletetoken'], - 'title' => 'UTPage' ), $data ); + 'title' => 'UTPage' ) ); $this->assertArrayHasKey( 'delete', $data[0] ); $this->assertArrayHasKey( 'title', $data[0]['delete'] ); $data = $this->doApiRequest( array( 'action' => 'query', - 'list' => 'watchlist' ), $data ); + 'list' => 'watchlist' ) ); $this->markTestIncomplete( 'This test needs to verify the deleted article was added to the users watchlist' ); } diff --git a/tests/phpunit/includes/api/PrefixUniquenessTest.php b/tests/phpunit/includes/api/PrefixUniquenessTest.php new file mode 100644 index 00000000..69b01ea7 --- /dev/null +++ b/tests/phpunit/includes/api/PrefixUniquenessTest.php @@ -0,0 +1,24 @@ +<?php + +/** + * Checks that all API query modules, core and extensions, have unique prefixes + * @group API + */ +class PrefixUniquenessTest extends MediaWikiTestCase { + public function testPrefixes() { + $main = new ApiMain( new FauxRequest() ); + $query = new ApiQuery( $main, 'foo', 'bar' ); + $modules = $query->getModules(); + $prefixes = array(); + + foreach ( $modules as $name => $class ) { + $module = new $class( $main, $name ); + $prefix = $module->getModulePrefix(); + if ( isset( $prefixes[$prefix] ) ) { + $this->fail( "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" ); + } + $prefixes[$module->getModulePrefix()] = $class; + } + $this->assertTrue( true ); // dummy call to make this test non-incomplete + } +} diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php index 86c0a828..8b6a3849 100644 --- a/tests/phpunit/includes/api/RandomImageGenerator.php +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -79,7 +79,7 @@ class RandomImageGenerator { foreach ( array( '/usr/share/dict/words', '/usr/dict/words', - dirname( __FILE__ ) . '/words.txt' ) + __DIR__ . '/words.txt' ) as $dictionaryFile ) { if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) { $this->dictionaryFile = $dictionaryFile; diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php index f3a14e5b..ee345623 100644 --- a/tests/phpunit/includes/api/generateRandomImages.php +++ b/tests/phpunit/includes/api/generateRandomImages.php @@ -6,14 +6,18 @@ */ // Evaluate the include path relative to this file -$IP = dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) ); +$IP = dirname( dirname( dirname( dirname( __DIR__ ) ) ) ); // Start up MediaWiki in command-line mode require_once( "$IP/maintenance/Maintenance.php" ); -require("RandomImageGenerator.php"); +require( __DIR__ . "/RandomImageGenerator.php" ); class GenerateRandomImages extends Maintenance { + public function getDbType() { + return Maintenance::DB_NONE; + } + public function execute() { $getOptSpec = array( diff --git a/tests/phpunit/includes/cache/GenderCacheTest.php b/tests/phpunit/includes/cache/GenderCacheTest.php new file mode 100644 index 00000000..a8b987e2 --- /dev/null +++ b/tests/phpunit/includes/cache/GenderCacheTest.php @@ -0,0 +1,101 @@ +<?php + +/** + * @group Database + * @group Cache + */ +class GenderCacheTest extends MediaWikiLangTestCase { + + function setUp() { + global $wgDefaultUserOptions; + parent::setUp(); + //ensure the correct default gender + $wgDefaultUserOptions['gender'] = 'unknown'; + } + + function addDBData() { + $user = User::newFromName( 'UTMale' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTMalePassword' ); + } + //ensure the right gender + $user->setOption( 'gender', 'male' ); + $user->saveSettings(); + + $user = User::newFromName( 'UTFemale' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTFemalePassword' ); + } + //ensure the right gender + $user->setOption( 'gender', 'female' ); + $user->saveSettings(); + + $user = User::newFromName( 'UTDefaultGender' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTDefaultGenderPassword' ); + } + //ensure the default gender + $user->setOption( 'gender', null ); + $user->saveSettings(); + } + + /** + * test usernames + * + * @dataProvider dataUserName + */ + function testUserName( $username, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $gender = $genderCache->getGenderOf( $username ); + $this->assertEquals( $gender, $expectedGender, "GenderCache normal" ); + } + + /** + * genderCache should work with user objects, too + * + * @dataProvider dataUserName + */ + function testUserObjects( $username, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $user = User::newFromName( $username ); + $gender = $genderCache->getGenderOf( $user ); + $this->assertEquals( $gender, $expectedGender, "GenderCache normal" ); + } + + function dataUserName() { + return array( + array( 'UTMale', 'male' ), + array( 'UTFemale', 'female' ), + array( 'UTDefaultGender', 'unknown' ), + array( 'UTNotExist', 'unknown' ), + //some not valid user + array( '127.0.0.1', 'unknown' ), + array( 'user@test', 'unknown' ), + ); + } + + /** + * test strip of subpages to avoid unnecessary queries + * against the never existing username + * + * @dataProvider dataStripSubpages + */ + function testStripSubpages( $pageWithSubpage, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $gender = $genderCache->getGenderOf( $pageWithSubpage ); + $this->assertEquals( $gender, $expectedGender, "GenderCache must strip of subpages" ); + } + + function dataStripSubpages() { + return array( + array( 'UTMale/subpage', 'male' ), + array( 'UTFemale/subpage', 'female' ), + array( 'UTDefaultGender/subpage', 'unknown' ), + array( 'UTNotExist/subpage', 'unknown' ), + array( '127.0.0.1/subpage', 'unknown' ), + ); + } +} diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php new file mode 100644 index 00000000..30bfb124 --- /dev/null +++ b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php @@ -0,0 +1,239 @@ +<?php + +/** + * Test for ProcessCacheLRU class. + * + * Note that it uses the ProcessCacheLRUTestable class which extends some + * properties and methods visibility. That class is defined at the end of the + * file containing this class. + * + * @group Cache + */ +class ProcessCacheLRUTest extends MediaWikiTestCase { + + /** + * Helper to verify emptiness of a cache object. + * Compare against an array so we get the cache content difference. + */ + function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) { + $this->assertAttributeEquals( array(), 'cache', $cache, $msg ); + } + + /** + * Helper to fill a cache object passed by reference + */ + function fillCache( &$cache, $numEntries ) { + // Fill cache with three values + for( $i=1; $i<=$numEntries; $i++) { + $cache->set( "cache-key-$i", "prop-$i", "value-$i" ); + } + } + + /** + * Generates an array of what would be expected in cache for a given cache + * size and a number of entries filled in sequentially + */ + function getExpectedCache( $cacheMaxEntries, $entryToFill ) { + $expected = array(); + + if( $entryToFill === 0 ) { + # The cache is empty! + return array(); + } elseif( $entryToFill <= $cacheMaxEntries ) { + # Cache is not fully filled + $firstKey = 1; + } else { + # Cache overflowed + $firstKey = 1 + $entryToFill - $cacheMaxEntries; + } + + $lastKey = $entryToFill; + + for( $i=$firstKey; $i<=$lastKey; $i++ ) { + $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" ); + } + return $expected; + } + + /** + * Highlight diff between assertEquals and assertNotSame + */ + function testPhpUnitArrayEquality() { + $one = array( 'A' => 1, 'B' => 2 ); + $two = array( 'B' => 2, 'A' => 1 ); + $this->assertEquals( $one, $two ); // == + $this->assertNotSame( $one, $two ); // === + } + + /** + * @dataProvider provideInvalidConstructorArg + * @expectedException MWException + */ + function testConstructorGivenInvalidValue( $maxSize ) { + $c = new ProcessCacheLRUTestable( $maxSize ); + } + + /** + * Value which are forbidden by the constructor + */ + function provideInvalidConstructorArg() { + return array( + array( null ), + array( array() ), + array( new stdClass() ), + array( 0 ), + array( '5' ), + array( -1 ), + ); + } + + function testAddAndGetAKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + // First set just one value + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $this->assertEquals( 1, $oneCache->getEntriesCount() ); + $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) ); + $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + function testDeleteOldKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $oneCache->set( 'cache-key', 'prop1', 'value2' ); + $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + /** + * This test that we properly overflow when filling a cache with + * a sequence of always different cache-keys. Meant to verify we correclty + * delete the older key. + * + * @dataProvider provideCacheFilling + * @param $cacheMaxEntries Maximum entry the created cache will hold + * @param $entryToFill Number of entries to insert in the created cache. + */ + function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) { + $cache = new ProcessCacheLRUTestable( $cacheMaxEntries ); + $this->fillCache( $cache, $entryToFill); + + $this->assertSame( + $this->getExpectedCache( $cacheMaxEntries, $entryToFill ), + $cache->getCache(), + "Filling a $cacheMaxEntries entries cache with $entryToFill entries" + ); + + } + + /** + * Provider for testFillingCache + */ + function provideCacheFilling() { + // ($cacheMaxEntries, $entryToFill, $msg='') + return array( + array( 1, 0 ), + array( 1, 1 ), + array( 1, 2 ), # overflow + array( 5, 33 ), # overflow + ); + + } + + /** + * Create a cache with only one remaining entry then update + * the first inserted entry. Should bump it to the top. + */ + function testReplaceExistingKeyShouldBumpEntryToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + // Fill cache leaving just one remaining slot + $this->fillCache( $cache, $maxEntries - 1 ); + + // Set an existing cache key + $cache->set( "cache-key-1", "prop-1", "new-value-for-1" ); + + $this->assertSame( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ), + ), + $cache->getCache() + ); + } + + function testRecentlyAccessedKeyStickIn() { + $cache = new ProcessCacheLRUTestable( 2 ); + $cache->set( 'first' , 'prop1', 'value1' ); + $cache->set( 'second', 'prop2', 'value2' ); + + // Get first + $cache->get( 'first', 'prop1' ); + // Cache a third value, should invalidate the least used one + $cache->set( 'third', 'prop3', 'value3' ); + + $this->assertFalse( $cache->has( 'second', 'prop2' ) ); + } + + /** + * This first create a full cache then update the value for the 2nd + * filled entry. + * Given a cache having 1,2,3 as key, updating 2 should bump 2 to + * the top of the queue with the new value: 1,3,2* (* = updated). + */ + function testReplaceExistingKeyInAFullCacheShouldBumpToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + $this->fillCache( $cache, $maxEntries ); + + // Set an existing cache key + $cache->set( "cache-key-2", "prop-2", "new-value-for-2" ); + $this->assertSame( + array( + 'cache-key-1' => array( 'prop-1' => 'value-1' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ), + ), + $cache->getCache() + ); + $this->assertEquals( 'new-value-for-2', + $cache->get( 'cache-key-2', 'prop-2' ) + ); + } + + function testBumpExistingKeyToTop() { + $cache = new ProcessCacheLRUTestable( 3 ); + $this->fillCache( $cache, 3 ); + + // Set the very first cache key to a new value + $cache->set( "cache-key-1", "prop-1", "new value for 1" ); + $this->assertEquals( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-1' => array( 'prop-1' => 'new value for 1' ), + ), + $cache->getCache() + ); + + } + +} + +/** + * Overrides some ProcessCacheLRU methods and properties accessibility. + */ +class ProcessCacheLRUTestable extends ProcessCacheLRU { + public $cache = array(); + + public function getCache() { + return $this->cache; + } + public function getEntriesCount() { + return count( $this->cache ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseSQLTest.php b/tests/phpunit/includes/db/DatabaseSQLTest.php new file mode 100644 index 00000000..e37cd445 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSQLTest.php @@ -0,0 +1,147 @@ +<?php + +/** + * Test the abstract database layer + * Using Mysql for the sql at the moment TODO + * + * @group Database + */ +class DatabaseSQLTest extends MediaWikiTestCase { + + public function setUp() { + // TODO support other DBMS or find another way to do it + if( $this->db->getType() !== 'mysql' ) { + $this->markTestSkipped( 'No mysql database' ); + } + } + + /** + * @dataProvider dataSelectSQLText + */ + function testSelectSQLText( $sql, $sqlText ) { + $this->assertEquals( trim( $this->db->selectSQLText( + isset( $sql['tables'] ) ? $sql['tables'] : array(), + isset( $sql['fields'] ) ? $sql['fields'] : array(), + isset( $sql['conds'] ) ? $sql['conds'] : array(), + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : array(), + isset( $sql['join_conds'] ) ? $sql['join_conds'] : array() + ) ), $sqlText ); + } + + function dataSelectSQLText() { + return array( + array( + array( + 'tables' => 'table', + 'fields' => array( 'field', 'alias' => 'field2' ), + 'conds' => array( 'alias' => 'text' ), + ), + "SELECT field,field2 AS alias " . + "FROM `unittest_table` " . + "WHERE alias = 'text'" + ), + array( + array( + 'tables' => 'table', + 'fields' => array( 'field', 'alias' => 'field2' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ), + ), + "SELECT field,field2 AS alias " . + "FROM `unittest_table` " . + "WHERE alias = 'text' " . + "ORDER BY field " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + )), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM `unittest_table` LEFT JOIN `unittest_table2` `t2` ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "ORDER BY field " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + )), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM `unittest_table` LEFT JOIN `unittest_table2` `t2` ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "GROUP BY field HAVING COUNT(*) > 1 " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'GROUP BY' => array( 'field', 'field2' ), 'HAVING' => array( 'COUNT(*) > 1', 'field' => 1 ) ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + )), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM `unittest_table` LEFT JOIN `unittest_table2` `t2` ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " . + "LIMIT 1" + ), + ); + } + + /** + * @dataProvider dataConditional + */ + function testConditional( $sql, $sqlText ) { + $this->assertEquals( trim( $this->db->conditional( + $sql['conds'], + $sql['true'], + $sql['false'] + ) ), $sqlText ); + } + + function dataConditional() { + return array( + array( + array( + 'conds' => array( 'field' => 'text' ), + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)" + ), + array( + array( + 'conds' => array( 'field' => 'text', 'field2' => 'anothertext' ), + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)" + ), + array( + array( + 'conds' => 'field=1', + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field=1 THEN 1 ELSE NULL END)" + ), + ); + } +}
\ No newline at end of file diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php index 067c731a..d226598b 100644 --- a/tests/phpunit/includes/db/DatabaseSqliteTest.php +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -250,6 +250,16 @@ class DatabaseSqliteTest extends MediaWikiTestCase { } } + public function testInsertIdType() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + $this->assertInstanceOf( 'ResultWrapper', + $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ ), "Database creationg" ); + $this->assertTrue( $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ ), + "Insertion worked" ); + $this->assertEquals( "integer", gettype( $db->insertId() ), "Actual typecheck" ); + $this->assertTrue( $db->close(), "closing database" ); + } + private function prepareDB( $version ) { static $maint = null; if ( $maint === null ) { diff --git a/tests/phpunit/includes/db/ORMRowTest.php b/tests/phpunit/includes/db/ORMRowTest.php new file mode 100644 index 00000000..9dcaf2b3 --- /dev/null +++ b/tests/phpunit/includes/db/ORMRowTest.php @@ -0,0 +1,234 @@ +<?php + +/** + * Abstract class to construct tests for ORMRow deriving classes. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.20 + * + * @ingroup Test + * + * @group ORM + * + * The database group has as a side effect that temporal database tables are created. This makes + * it possible to test without poisoning a production database. + * @group Database + * + * Some of the tests takes more time, and needs therefor longer time before they can be aborted + * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases + * that hold the first tests in a pending state awaiting access to the database. + * @group medium + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class ORMRowTest extends \MediaWikiTestCase { + + /** + * @since 1.20 + * @return string + */ + protected abstract function getRowClass(); + + /** + * @since 1.20 + * @return IORMTable + */ + protected abstract function getTableInstance(); + + /** + * @since 1.20 + * @return array + */ + public abstract function constructorTestProvider(); + + /** + * @since 1.20 + * @param IORMRow $row + * @param array $data + */ + protected function verifyFields( IORMRow $row, array $data ) { + foreach ( array_keys( $data ) as $fieldName ) { + $this->assertEquals( $data[$fieldName], $row->getField( $fieldName ) ); + } + } + + /** + * @since 1.20 + * @param array $data + * @param boolean $loadDefaults + * @return IORMRow + */ + protected function getRowInstance( array $data, $loadDefaults ) { + $class = $this->getRowClass(); + return new $class( $this->getTableInstance(), $data, $loadDefaults ); + } + + /** + * @since 1.20 + * @return array + */ + protected function getMockValues() { + return array( + 'id' => 1, + 'str' => 'foobar4645645', + 'int' => 42, + 'float' => 4.2, + 'bool' => true, + 'array' => array( 42, 'foobar' ), + 'blob' => new stdClass() + ); + } + + /** + * @since 1.20 + * @return array + */ + protected function getMockFields() { + $mockValues = $this->getMockValues(); + $mockFields = array(); + + foreach ( $this->getTableInstance()->getFields() as $name => $type ) { + if ( $name !== 'id' ) { + $mockFields[$name] = $mockValues[$type]; + } + } + + return $mockFields; + } + + /** + * @since 1.20 + * @return array of IORMRow + */ + public function instanceProvider() { + $instances = array(); + + foreach ( $this->constructorTestProvider() as $arguments ) { + $instances[] = array( call_user_func_array( array( $this, 'getRowInstance' ), $arguments ) ); + } + + return $instances; + } + + /** + * @dataProvider constructorTestProvider + */ + public function testConstructor( array $data, $loadDefaults ) { + $this->verifyFields( $this->getRowInstance( $data, $loadDefaults ), $data ); + } + + /** + * @dataProvider constructorTestProvider + */ + public function testSave( array $data, $loadDefaults ) { + $item = $this->getRowInstance( $data, $loadDefaults ); + + $this->assertTrue( $item->save() ); + + $this->assertTrue( $item->hasIdField() ); + $this->assertTrue( is_integer( $item->getId() ) ); + + $id = $item->getId(); + + $this->assertTrue( $item->save() ); + + $this->assertEquals( $id, $item->getId() ); + + $this->verifyFields( $item, $data ); + } + + /** + * @dataProvider constructorTestProvider + */ + public function testRemove( array $data, $loadDefaults ) { + $item = $this->getRowInstance( $data, $loadDefaults ); + + $this->assertTrue( $item->save() ); + + $this->assertTrue( $item->remove() ); + + $this->assertFalse( $item->hasIdField() ); + + $this->assertTrue( $item->save() ); + + $this->verifyFields( $item, $data ); + + $this->assertTrue( $item->remove() ); + + $this->assertFalse( $item->hasIdField() ); + + $this->verifyFields( $item, $data ); + } + + /** + * @dataProvider instanceProvider + */ + public function testSetField( IORMRow $item ) { + foreach ( $this->getMockFields() as $name => $value ) { + $item->setField( $name, $value ); + $this->assertEquals( $value, $item->getField( $name ) ); + } + } + + /** + * @since 1.20 + * @param array $expected + * @param IORMRow $item + */ + protected function assertFieldValues( array $expected, IORMRow $item ) { + foreach ( $expected as $name => $type ) { + if ( $name !== 'id' ) { + $this->assertEquals( $expected[$name], $item->getField( $name ) ); + } + } + } + + /** + * @dataProvider instanceProvider + */ + public function testSetFields( IORMRow $item ) { + $originalValues = $item->getFields(); + + $item->setFields( array(), false ); + + foreach ( $item->getTable()->getFields() as $name => $type ) { + $originalHas = array_key_exists( $name, $originalValues ); + $newHas = $item->hasField( $name ); + + $this->assertEquals( $originalHas, $newHas ); + + if ( $originalHas && $newHas ) { + $this->assertEquals( $originalValues[$name], $item->getField( $name ) ); + } + } + + $mockFields = $this->getMockFields(); + + $item->setFields( $mockFields, false ); + + $this->assertFieldValues( $originalValues, $item ); + + $item->setFields( $mockFields, true ); + + $this->assertFieldValues( $mockFields, $item ); + } + + // TODO: test all of the methods! + +}
\ No newline at end of file diff --git a/tests/phpunit/includes/db/TestORMRowTest.php b/tests/phpunit/includes/db/TestORMRowTest.php new file mode 100644 index 00000000..afd1cb80 --- /dev/null +++ b/tests/phpunit/includes/db/TestORMRowTest.php @@ -0,0 +1,174 @@ +<?php + +/** + * Tests for the TestORMRow class. + * TestORMRow is a dummy class to be able to test the abstract ORMRow class. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.20 + * + * @ingroup Test + * + * @group ORM + * + * The database group has as a side effect that temporal database tables are created. This makes + * it possible to test without poisoning a production database. + * @group Database + * + * Some of the tests takes more time, and needs therefor longer time before they can be aborted + * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases + * that hold the first tests in a pending state awaiting access to the database. + * @group medium + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +require_once __DIR__ . "/ORMRowTest.php"; + +class TestORMRowTest extends ORMRowTest { + + /** + * @since 1.20 + * @return string + */ + protected function getRowClass() { + return 'TestORMRow'; + } + + /** + * @since 1.20 + * @return IORMTable + */ + protected function getTableInstance() { + return TestORMTable::singleton(); + } + + public function setUp() { + parent::setUp(); + + $dbw = wfGetDB( DB_MASTER ); + + $isSqlite = $GLOBALS['wgDBtype'] === 'sqlite'; + + $idField = $isSqlite ? 'INTEGER' : 'INT unsigned'; + $primaryKey = $isSqlite ? 'PRIMARY KEY AUTOINCREMENT' : 'auto_increment PRIMARY KEY'; + + $dbw->query( + 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . '( + test_id ' . $idField . ' NOT NULL ' . $primaryKey . ', + test_name VARCHAR(255) NOT NULL, + test_age TINYINT unsigned NOT NULL, + test_height FLOAT NOT NULL, + test_awesome TINYINT unsigned NOT NULL, + test_stuff BLOB NOT NULL, + test_moarstuff BLOB NOT NULL, + test_time varbinary(14) NOT NULL + );' + ); + } + + public function constructorTestProvider() { + return array( + array( + array( + 'name' => 'Foobar', + 'age' => 42, + 'height' => 9000.1, + 'awesome' => true, + 'stuff' => array( 13, 11, 7, 5, 3, 2 ), + 'moarstuff' => (object)array( 'foo' => 'bar', 'bar' => array( 4, 2 ), 'baz' => true ) + ), + true + ), + ); + } + +} + +class TestORMRow extends ORMRow {} + +class TestORMTable extends ORMTable { + + /** + * Returns the name of the database table objects of this type are stored in. + * + * @since 1.20 + * + * @return string + */ + public function getName() { + return 'orm_test'; + } + + /** + * Returns the name of a IORMRow implementing class that + * represents single rows in this table. + * + * @since 1.20 + * + * @return string + */ + public function getRowClass() { + return 'TestORMRow'; + } + + /** + * Returns an array with the fields and their types this object contains. + * This corresponds directly to the fields in the database, without prefix. + * + * field name => type + * + * Allowed types: + * * id + * * str + * * int + * * float + * * bool + * * array + * * blob + * + * @since 1.20 + * + * @return array + */ + public function getFields() { + return array( + 'id' => 'id', + 'name' => 'str', + 'age' => 'int', + 'height' => 'float', + 'awesome' => 'bool', + 'stuff' => 'array', + 'moarstuff' => 'blob', + 'time' => 'int', // TS_MW + ); + } + + /** + * Gets the db field prefix. + * + * @since 1.20 + * + * @return string + */ + protected function getFieldPrefix() { + return 'test_'; + } + + +} diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php index 5a4e66d4..246b2918 100644 --- a/tests/phpunit/includes/debug/MWDebugTest.php +++ b/tests/phpunit/includes/debug/MWDebugTest.php @@ -12,6 +12,11 @@ class MWDebugTest extends MediaWikiTestCase { } /** Clear log before each test */ MWDebug::clearLog(); + wfSuppressWarnings(); + } + + function tearDown() { + wfRestoreWarnings(); } function testAddLog() { @@ -30,7 +35,7 @@ class MWDebugTest extends MediaWikiTestCase { $this->assertEquals( array( array( 'msg' => 'Warning message', 'type' => 'warn', - 'caller' => 'MWDebug::warning', + 'caller' => 'MWDebugTest::testAddWarning', ) ), MWDebug::getLog() ); diff --git a/tests/phpunit/includes/filerepo/FileBackendTest.php b/tests/phpunit/includes/filerepo/FileBackendTest.php index da44797a..a2dc5c6c 100644 --- a/tests/phpunit/includes/filerepo/FileBackendTest.php +++ b/tests/phpunit/includes/filerepo/FileBackendTest.php @@ -3,11 +3,11 @@ /** * @group FileRepo * @group FileBackend + * @group medium */ class FileBackendTest extends MediaWikiTestCase { private $backend, $multiBackend; private $filesToPrune = array(); - private $dirsToPrune = array(); private static $backendToUse; function setUp() { @@ -23,10 +23,14 @@ class FileBackendTest extends MediaWikiTestCase { foreach ( $wgFileBackends as $conf ) { if ( $conf['name'] == $name ) { $useConfig = $conf; + break; } } $useConfig['name'] = 'localtesting'; // swap name - $class = $conf['class']; + $useConfig['shardViaHashLevels'] = array( // test sharding + 'unittest-cont1' => array( 'levels' => 1, 'base' => 16, 'repeat' => 1 ) + ); + $class = $useConfig['class']; self::$backendToUse = new $class( $useConfig ); $this->singleBackend = self::$backendToUse; } @@ -34,6 +38,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->singleBackend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'fsLockManager', + #'parallelize' => 'implicit', 'containerPaths' => array( 'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1", 'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" ) @@ -42,6 +47,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->multiBackend = new FileBackendMultiWrite( array( 'name' => 'localtesting', 'lockManager' => 'fsLockManager', + 'parallelize' => 'implicit', 'backends' => array( array( 'name' => 'localmutlitesting1', @@ -204,7 +210,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - function doTestStore( $op ) { + private function doTestStore( $op ) { $backendName = $this->backendClass(); $source = $op['src']; @@ -219,9 +225,9 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( $op ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Store from $source to $dest succeeded without warnings ($backendName)." ); - $this->assertEquals( array(), $status->errors, + $this->assertEquals( true, $status->isOK(), "Store from $source to $dest succeeded ($backendName)." ); $this->assertEquals( array( 0 => true ), $status->success, "Store from $source to $dest has proper 'success' field in Status ($backendName)." ); @@ -238,13 +244,15 @@ class FileBackendTest extends MediaWikiTestCase { $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); $this->assertEquals( $props1, $props2, "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $dest ) ); } public function provider_testStore() { $cases = array(); $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath(); - $toPath = $this->baseStorePath() . '/unittest-cont1/fun/obj1.txt'; + $toPath = $this->baseStorePath() . '/unittest-cont1/e/fun/obj1.txt'; $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ); $cases[] = array( $op, // operation @@ -286,7 +294,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - function doTestCopy( $op ) { + private function doTestCopy( $op ) { $backendName = $this->backendClass(); $source = $op['src']; @@ -296,7 +304,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { @@ -305,7 +313,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( $op ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Copy from $source to $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Copy from $source to $dest succeeded ($backendName)." ); @@ -325,13 +333,15 @@ class FileBackendTest extends MediaWikiTestCase { $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); $this->assertEquals( $props1, $props2, "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source, $dest ) ); } public function provider_testCopy() { $cases = array(); - $source = $this->baseStorePath() . '/unittest-cont1/file.txt'; - $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt'; + $source = $this->baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = $this->baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest ); $cases[] = array( @@ -384,7 +394,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { @@ -392,7 +402,7 @@ class FileBackendTest extends MediaWikiTestCase { } $status = $this->backend->doOperation( $op ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Move from $source to $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Move from $source to $dest succeeded ($backendName)." ); @@ -414,13 +424,15 @@ class FileBackendTest extends MediaWikiTestCase { "Source file does not exist accourding to props ($backendName)." ); $this->assertEquals( true, $props2['fileExists'], "Destination file exists accourding to props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source, $dest ) ); } public function provider_testMove() { $cases = array(); - $source = $this->baseStorePath() . '/unittest-cont1/file.txt'; - $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt'; + $source = $this->baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = $this->baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest ); $cases[] = array( @@ -472,13 +484,13 @@ class FileBackendTest extends MediaWikiTestCase { if ( $withSource ) { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); } $status = $this->backend->doOperation( $op ); if ( $okStatus ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Deletion of file at $source succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Deletion of file at $source succeeded ($backendName)." ); @@ -499,12 +511,14 @@ class FileBackendTest extends MediaWikiTestCase { $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); $this->assertFalse( $props1['fileExists'], "Source file $source does not exist according to props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source ) ); } public function provider_testDelete() { $cases = array(); - $source = $this->baseStorePath() . '/unittest-cont1/myfacefile.txt'; + $source = $this->baseStorePath() . '/unittest-cont1/e/myfacefile.txt'; $op = array( 'op' => 'delete', 'src' => $source ); $cases[] = array( @@ -554,13 +568,13 @@ class FileBackendTest extends MediaWikiTestCase { if ( $alreadyExists ) { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $dest succeeded ($backendName)." ); } $status = $this->backend->doOperation( $op ); if ( $okStatus ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of file at $dest succeeded ($backendName)." ); @@ -590,6 +604,8 @@ class FileBackendTest extends MediaWikiTestCase { $this->backend->getFileSize( array( 'src' => $dest ) ), "Destination file $dest has original size according to props ($backendName)." ); } + + $this->assertBackendPathsConsistent( array( $dest ) ); } /** @@ -598,7 +614,7 @@ class FileBackendTest extends MediaWikiTestCase { public function provider_testCreate() { $cases = array(); - $dest = $this->baseStorePath() . '/unittest-cont2/myspacefile.txt'; + $dest = $this->baseStorePath() . '/unittest-cont2/a/myspacefile.txt'; $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest ); $cases[] = array( @@ -649,6 +665,54 @@ class FileBackendTest extends MediaWikiTestCase { return $cases; } + public function testDoQuickOperations() { + $this->backend = $this->singleBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + } + + private function doTestDoQuickOperations() { + $backendName = $this->backendClass(); + + $base = $this->baseStorePath(); + $files = array( + "$base/unittest-cont1/e/fileA.a", + "$base/unittest-cont1/e/fileB.a", + "$base/unittest-cont1/e/fileC.a" + ); + $ops = array(); + $purgeOps = array(); + foreach ( $files as $path ) { + $status = $this->prepare( array( 'dir' => dirname( $path ) ) ); + $this->assertGoodStatus( $status, + "Preparing $path succeeded without warnings ($backendName)." ); + $ops[] = array( 'op' => 'create', 'dst' => $path, 'content' => mt_rand(0,50000) ); + $purgeOps[] = array( 'op' => 'delete', 'src' => $path ); + } + $purgeOps[] = array( 'op' => 'null' ); + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, + "Creation of source files succeeded ($backendName)." ); + + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( array( 'src' => $file ) ), + "File $file exists." ); + } + + $status = $this->backend->doQuickOperations( $purgeOps ); + $this->assertGoodStatus( $status, + "Quick deletion of source files succeeded ($backendName)." ); + + foreach ( $files as $file ) { + $this->assertFalse( $this->backend->fileExists( array( 'src' => $file ) ), + "File $file purged." ); + } + } + /** * @dataProvider provider_testConcatenate */ @@ -667,7 +731,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - public function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) { + private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) { $backendName = $this->backendClass(); $expContent = ''; @@ -684,7 +748,7 @@ class FileBackendTest extends MediaWikiTestCase { } $status = $this->backend->doOperations( $ops ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of source files succeeded ($backendName)." ); $dest = $params['dst']; @@ -701,7 +765,7 @@ class FileBackendTest extends MediaWikiTestCase { // Combine the files into one $status = $this->backend->concatenate( $params ); if ( $okStatus ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of concat file at $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of concat file at $dest succeeded ($backendName)." ); @@ -736,16 +800,16 @@ class FileBackendTest extends MediaWikiTestCase { $rand = mt_rand( 0, 2000000000 ) . time(); $dest = wfTempDir() . "/randomfile!$rand.txt"; $srcs = array( - $this->baseStorePath() . '/unittest-cont1/file1.txt', - $this->baseStorePath() . '/unittest-cont1/file2.txt', - $this->baseStorePath() . '/unittest-cont1/file3.txt', - $this->baseStorePath() . '/unittest-cont1/file4.txt', - $this->baseStorePath() . '/unittest-cont1/file5.txt', - $this->baseStorePath() . '/unittest-cont1/file6.txt', - $this->baseStorePath() . '/unittest-cont1/file7.txt', - $this->baseStorePath() . '/unittest-cont1/file8.txt', - $this->baseStorePath() . '/unittest-cont1/file9.txt', - $this->baseStorePath() . '/unittest-cont1/file10.txt' + $this->baseStorePath() . '/unittest-cont1/e/file1.txt', + $this->baseStorePath() . '/unittest-cont1/e/file2.txt', + $this->baseStorePath() . '/unittest-cont1/e/file3.txt', + $this->baseStorePath() . '/unittest-cont1/e/file4.txt', + $this->baseStorePath() . '/unittest-cont1/e/file5.txt', + $this->baseStorePath() . '/unittest-cont1/e/file6.txt', + $this->baseStorePath() . '/unittest-cont1/e/file7.txt', + $this->baseStorePath() . '/unittest-cont1/e/file8.txt', + $this->baseStorePath() . '/unittest-cont1/e/file9.txt', + $this->baseStorePath() . '/unittest-cont1/e/file10.txt' ); $content = array( 'egfage', @@ -800,8 +864,8 @@ class FileBackendTest extends MediaWikiTestCase { if ( $alreadyExists ) { $this->prepare( array( 'dir' => dirname( $path ) ) ); - $status = $this->backend->create( array( 'dst' => $path, 'content' => $content ) ); - $this->assertEquals( array(), $status->errors, + $status = $this->create( array( 'dst' => $path, 'content' => $content ) ); + $this->assertGoodStatus( $status, "Creation of file at $path succeeded ($backendName)." ); $size = $this->backend->getFileSize( array( 'src' => $path ) ); @@ -810,20 +874,34 @@ class FileBackendTest extends MediaWikiTestCase { $this->assertEquals( strlen( $content ), $size, "Correct file size of '$path'" ); - $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5, + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, "Correct file timestamp of '$path'" ); $size = $stat['size']; $time = $stat['mtime']; $this->assertEquals( strlen( $content ), $size, "Correct file size of '$path'" ); - $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5, + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, "Correct file timestamp of '$path'" ); + + $this->backend->clearCache( array( $path ) ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + + $this->backend->preloadCache( array( $path ) ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); } else { $size = $this->backend->getFileSize( array( 'src' => $path ) ); $time = $this->backend->getFileTimestamp( array( 'src' => $path ) ); $stat = $this->backend->getFileStat( array( 'src' => $path ) ); - + $this->assertFalse( $size, "Correct file size of '$path'" ); $this->assertFalse( $time, "Correct file timestamp of '$path'" ); $this->assertFalse( $stat, "Correct file stat of '$path'" ); @@ -834,9 +912,9 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents", true ); - $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "", true ); - $cases[] = array( "$base/unittest-cont1/b/some-diff_file.txt", null, false ); + $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true ); + $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "", true ); + $cases[] = array( "$base/unittest-cont1/e/b/some-diff_file.txt", null, false ); return $cases; } @@ -856,14 +934,14 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - public function doTestGetFileContents( $source, $content ) { + private function doTestGetFileContents( $source, $content ) { $backendName = $this->backendClass(); $this->prepare( array( 'dir' => dirname( $source ) ) ); $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded with OK status ($backendName)." ); @@ -880,8 +958,8 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents" ); - $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" ); return $cases; } @@ -901,14 +979,14 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - public function doTestGetLocalCopy( $source, $content ) { + private function doTestGetLocalCopy( $source, $content ) { $backendName = $this->backendClass(); $this->prepare( array( 'dir' => dirname( $source ) ) ); $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) ); @@ -923,8 +1001,8 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" ); - $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); return $cases; } @@ -949,9 +1027,8 @@ class FileBackendTest extends MediaWikiTestCase { $this->prepare( array( 'dir' => dirname( $source ) ) ); - $status = $this->backend->doOperation( - array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $status = $this->create( array( 'content' => $content, 'dst' => $source ) ); + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) ); @@ -966,8 +1043,8 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" ); - $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); return $cases; } @@ -988,19 +1065,19 @@ class FileBackendTest extends MediaWikiTestCase { function provider_testPrepareAndClean() { $base = $this->baseStorePath(); return array( - array( "$base/unittest-cont1/a/z/some_file1.txt", true ), + array( "$base/unittest-cont1/e/a/z/some_file1.txt", true ), array( "$base/unittest-cont2/a/z/some_file2.txt", true ), # Specific to FS backend with no basePath field set #array( "$base/unittest-cont3/a/z/some_file3.txt", false ), ); } - function doTestPrepareAndClean( $path, $isOK ) { + private function doTestPrepareAndClean( $path, $isOK ) { $backendName = $this->backendClass(); $status = $this->prepare( array( 'dir' => dirname( $path ) ) ); if ( $isOK ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Preparing dir $path succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Preparing dir $path succeeded ($backendName)." ); @@ -1011,7 +1088,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) ); if ( $isOK ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Cleaning dir $path succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Cleaning dir $path succeeded ($backendName)." ); @@ -1021,6 +1098,58 @@ class FileBackendTest extends MediaWikiTestCase { } } + public function testRecursiveClean() { + $this->backend = $this->singleBackend; + $this->doTestRecursiveClean(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestRecursiveClean(); + $this->tearDownFiles(); + } + + private function doTestRecursiveClean() { + $backendName = $this->backendClass(); + + $base = $this->baseStorePath(); + $dirs = array( + "$base/unittest-cont1/e/a", + "$base/unittest-cont1/e/a/b", + "$base/unittest-cont1/e/a/b/c", + "$base/unittest-cont1/e/a/b/c/d0", + "$base/unittest-cont1/e/a/b/c/d1", + "$base/unittest-cont1/e/a/b/c/d2", + "$base/unittest-cont1/e/a/b/c/d0/1", + "$base/unittest-cont1/e/a/b/c/d0/2", + "$base/unittest-cont1/e/a/b/c/d1/3", + "$base/unittest-cont1/e/a/b/c/d1/4", + "$base/unittest-cont1/e/a/b/c/d2/5", + "$base/unittest-cont1/e/a/b/c/d2/6" + ); + foreach ( $dirs as $dir ) { + $status = $this->prepare( array( 'dir' => $dir ) ); + $this->assertGoodStatus( $status, + "Preparing dir $dir succeeded without warnings ($backendName)." ); + } + + if ( $this->backend instanceof FSFileBackend ) { + foreach ( $dirs as $dir ) { + $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ), + "Dir $dir exists ($backendName)." ); + } + } + + $status = $this->backend->clean( + array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) ); + $this->assertGoodStatus( $status, + "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." ); + + foreach ( $dirs as $dir ) { + $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ), + "Dir $dir no longer exists ($backendName)." ); + } + } + // @TODO: testSecure public function testDoOperations() { @@ -1033,39 +1162,127 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); $this->doTestDoOperations(); $this->tearDownFiles(); + } + + private function doTestDoOperations() { + $base = $this->baseStorePath(); + + $fileA = "$base/unittest-cont1/e/a/b/fileA.txt"; + $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; + $fileB = "$base/unittest-cont1/e/a/b/fileB.txt"; + $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; + $fileC = "$base/unittest-cont1/e/a/b/fileC.txt"; + $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; + $fileD = "$base/unittest-cont1/e/a/b/fileD.txt"; + + $this->prepare( array( 'dir' => dirname( $fileA ) ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->prepare( array( 'dir' => dirname( $fileB ) ) ); + $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); + $this->prepare( array( 'dir' => dirname( $fileC ) ) ); + $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->prepare( array( 'dir' => dirname( $fileD ) ) ); + + $status = $this->backend->doOperations( array( + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), + // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>) + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:<A>, B:<B>, C:<A>, D:<empty> + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ), + // Now: A:<A>, B:<B>, C:<empty>, D:<A> + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ), + // Now: A:<A>, B:<empty>, C:<B>, D:<A> + array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:<A>, B:<empty>, C:<B>, D:<empty> + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ), + // Now: A:<B>, B:<empty>, C:<empty>, D:<empty> + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ), + // Now: A:<B>, B:<empty>, C:<B>, D:<empty> + array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Now: A:<empty>, B:<empty>, C:<B>, D:<empty> + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'null' ), + // Does nothing + ) ); + $this->assertGoodStatus( $status, "Operation batch succeeded" ); + $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); + $this->assertEquals( 13, count( $status->success ), + "Operation batch has correct success array" ); + + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), + "File does not exist at $fileA" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ), + "File does not exist at $fileB" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ), + "File does not exist at $fileD" ); + + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ), + "File exists at $fileC" ); + $this->assertEquals( $fileBContents, + $this->backend->getFileContents( array( 'src' => $fileC ) ), + "Correct file contents of $fileC" ); + $this->assertEquals( strlen( $fileBContents ), + $this->backend->getFileSize( array( 'src' => $fileC ) ), + "Correct file size of $fileC" ); + $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ), + $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ), + "Correct file SHA-1 of $fileC" ); + } + + public function testDoOperationsPipeline() { $this->backend = $this->singleBackend; $this->tearDownFiles(); - $this->doTestDoOperationsFailing(); + $this->doTestDoOperationsPipeline(); $this->tearDownFiles(); $this->backend = $this->multiBackend; $this->tearDownFiles(); - $this->doTestDoOperationsFailing(); + $this->doTestDoOperationsPipeline(); $this->tearDownFiles(); - - // @TODO: test some cases where the ops should fail } - function doTestDoOperations() { + // concurrency orientated + private function doTestDoOperationsPipeline() { $base = $this->baseStorePath(); - $fileA = "$base/unittest-cont1/a/b/fileA.txt"; $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; - $fileB = "$base/unittest-cont1/a/b/fileB.txt"; $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; - $fileC = "$base/unittest-cont1/a/b/fileC.txt"; $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; - $fileD = "$base/unittest-cont1/a/b/fileD.txt"; + + $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameA, $fileAContents ); + $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameB, $fileBContents ); + $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameC, $fileCContents ); + + $this->filesToPrune[] = $tmpNameA; # avoid file leaking + $this->filesToPrune[] = $tmpNameB; # avoid file leaking + $this->filesToPrune[] = $tmpNameC; # avoid file leaking + + $fileA = "$base/unittest-cont1/e/a/b/fileA.txt"; + $fileB = "$base/unittest-cont1/e/a/b/fileB.txt"; + $fileC = "$base/unittest-cont1/e/a/b/fileC.txt"; + $fileD = "$base/unittest-cont1/e/a/b/fileD.txt"; $this->prepare( array( 'dir' => dirname( $fileA ) ) ); - $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); $this->prepare( array( 'dir' => dirname( $fileB ) ) ); - $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); $this->prepare( array( 'dir' => dirname( $fileC ) ) ); - $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->prepare( array( 'dir' => dirname( $fileD ) ) ); $status = $this->backend->doOperations( array( + array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ), + array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ), + array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ), array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>) array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), @@ -1094,9 +1311,9 @@ class FileBackendTest extends MediaWikiTestCase { // Does nothing ) ); - $this->assertEquals( array(), $status->errors, "Operation batch succeeded" ); + $this->assertGoodStatus( $status, "Operation batch succeeded" ); $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); - $this->assertEquals( 13, count( $status->success ), + $this->assertEquals( 16, count( $status->success ), "Operation batch has correct success array" ); $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), @@ -1119,7 +1336,19 @@ class FileBackendTest extends MediaWikiTestCase { "Correct file SHA-1 of $fileC" ); } - function doTestDoOperationsFailing() { + public function testDoOperationsFailing() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsFailing(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsFailing(); + $this->tearDownFiles(); + } + + private function doTestDoOperationsFailing() { $base = $this->baseStorePath(); $fileA = "$base/unittest-cont2/a/b/fileA.txt"; @@ -1131,11 +1360,11 @@ class FileBackendTest extends MediaWikiTestCase { $fileD = "$base/unittest-cont2/a/b/fileD.txt"; $this->prepare( array( 'dir' => dirname( $fileA ) ) ); - $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); $this->prepare( array( 'dir' => dirname( $fileB ) ) ); - $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); + $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); $this->prepare( array( 'dir' => dirname( $fileC ) ) ); - $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); $status = $this->backend->doOperations( array( array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), @@ -1195,23 +1424,26 @@ class FileBackendTest extends MediaWikiTestCase { private function doTestGetFileList() { $backendName = $this->backendClass(); - $base = $this->baseStorePath(); + + // Should have no errors + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) ); + $files = array( - "$base/unittest-cont1/test1.txt", - "$base/unittest-cont1/test2.txt", - "$base/unittest-cont1/test3.txt", - "$base/unittest-cont1/subdir1/test1.txt", - "$base/unittest-cont1/subdir1/test2.txt", - "$base/unittest-cont1/subdir2/test3.txt", - "$base/unittest-cont1/subdir2/test4.txt", - "$base/unittest-cont1/subdir2/subdir/test1.txt", - "$base/unittest-cont1/subdir2/subdir/test2.txt", - "$base/unittest-cont1/subdir2/subdir/test3.txt", - "$base/unittest-cont1/subdir2/subdir/test4.txt", - "$base/unittest-cont1/subdir2/subdir/test5.txt", - "$base/unittest-cont1/subdir2/subdir/sub/test0.txt", - "$base/unittest-cont1/subdir2/subdir/sub/120-px-file.txt", + "$base/unittest-cont1/e/test1.txt", + "$base/unittest-cont1/e/test2.txt", + "$base/unittest-cont1/e/test3.txt", + "$base/unittest-cont1/e/subdir1/test1.txt", + "$base/unittest-cont1/e/subdir1/test2.txt", + "$base/unittest-cont1/e/subdir2/test3.txt", + "$base/unittest-cont1/e/subdir2/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test1.txt", + "$base/unittest-cont1/e/subdir2/subdir/test2.txt", + "$base/unittest-cont1/e/subdir2/subdir/test3.txt", + "$base/unittest-cont1/e/subdir2/subdir/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test5.txt", + "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt", + "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt", ); // Add the files @@ -1220,28 +1452,28 @@ class FileBackendTest extends MediaWikiTestCase { $this->prepare( array( 'dir' => dirname( $file ) ) ); $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); } - $status = $this->backend->doOperations( $ops ); - $this->assertEquals( array(), $status->errors, + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, "Creation of files succeeded ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of files succeeded with OK status ($backendName)." ); // Expected listing $expected = array( - "test1.txt", - "test2.txt", - "test3.txt", - "subdir1/test1.txt", - "subdir1/test2.txt", - "subdir2/test3.txt", - "subdir2/test4.txt", - "subdir2/subdir/test1.txt", - "subdir2/subdir/test2.txt", - "subdir2/subdir/test3.txt", - "subdir2/subdir/test4.txt", - "subdir2/subdir/test5.txt", - "subdir2/subdir/sub/test0.txt", - "subdir2/subdir/sub/120-px-file.txt", + "e/test1.txt", + "e/test2.txt", + "e/test3.txt", + "e/subdir1/test1.txt", + "e/subdir1/test2.txt", + "e/subdir2/test3.txt", + "e/subdir2/test4.txt", + "e/subdir2/subdir/test1.txt", + "e/subdir2/subdir/test2.txt", + "e/subdir2/subdir/test3.txt", + "e/subdir2/subdir/test4.txt", + "e/subdir2/subdir/test5.txt", + "e/subdir2/subdir/sub/test0.txt", + "e/subdir2/subdir/sub/120-px-file.txt", ); sort( $expected ); @@ -1279,7 +1511,7 @@ class FileBackendTest extends MediaWikiTestCase { // Actual listing (no trailing slash) $list = array(); - $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) ); + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ); foreach ( $iter as $file ) { $list[] = $file; } @@ -1289,7 +1521,7 @@ class FileBackendTest extends MediaWikiTestCase { // Actual listing (with trailing slash) $list = array(); - $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir/" ) ); + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ) ); foreach ( $iter as $file ) { $list[] = $file; } @@ -1306,6 +1538,26 @@ class FileBackendTest extends MediaWikiTestCase { $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." ); + // Expected listing (top files only) + $expected = array( + "test1.txt", + "test2.txt", + "test3.txt", + "test4.txt", + "test5.txt" + ); + sort( $expected ); + + // Actual listing (top files only) + $list = array(); + $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." ); + foreach ( $files as $file ) { // clean up $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); } @@ -1314,12 +1566,268 @@ class FileBackendTest extends MediaWikiTestCase { foreach ( $iter as $iter ) {} // no errors } + public function testGetDirectoryList() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetDirectoryList(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetDirectoryList(); + $this->tearDownFiles(); + } + + private function doTestGetDirectoryList() { + $backendName = $this->backendClass(); + + $base = $this->baseStorePath(); + $files = array( + "$base/unittest-cont1/e/test1.txt", + "$base/unittest-cont1/e/test2.txt", + "$base/unittest-cont1/e/test3.txt", + "$base/unittest-cont1/e/subdir1/test1.txt", + "$base/unittest-cont1/e/subdir1/test2.txt", + "$base/unittest-cont1/e/subdir2/test3.txt", + "$base/unittest-cont1/e/subdir2/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test1.txt", + "$base/unittest-cont1/e/subdir3/subdir/test2.txt", + "$base/unittest-cont1/e/subdir4/subdir/test3.txt", + "$base/unittest-cont1/e/subdir4/subdir/test4.txt", + "$base/unittest-cont1/e/subdir4/subdir/test5.txt", + "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt", + "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt", + ); + + // Add the files + $ops = array(); + foreach ( $files as $file ) { + $this->prepare( array( 'dir' => dirname( $file ) ) ); + $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); + } + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, + "Creation of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of files succeeded with OK status ($backendName)." ); + + $this->assertEquals( true, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ), + "Directory exists in ($backendName)." ); + $this->assertEquals( true, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ), + "Directory exists in ($backendName)." ); + $this->assertEquals( false, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ) ), + "Directory does not exists in ($backendName)." ); + + // Expected listing + $expected = array( + "e", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Expected listing + $expected = array( + "subdir1", + "subdir2", + "subdir3", + "subdir4", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (with trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Expected listing + $expected = array( + "subdir", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (with trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (using iterator second time) + $list = array(); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." ); + + // Expected listing (recursive) + $expected = array( + "e", + "e/subdir1", + "e/subdir2", + "e/subdir3", + "e/subdir4", + "e/subdir2/subdir", + "e/subdir3/subdir", + "e/subdir4/subdir", + "e/subdir4/subdir/sub", + ); + sort( $expected ); + + // Actual listing (recursive) + $list = array(); + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + // Expected listing (recursive) + $expected = array( + "subdir", + "subdir/sub", + ); + sort( $expected ); + + // Actual listing (recursive) + $list = array(); + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir4" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + // Actual listing (recursive, second time) + $list = array(); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + foreach ( $files as $file ) { // clean up + $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); + } + + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) ); + foreach ( $iter as $iter ) {} // no errors + } + + public function testLockCalls() { + $this->backend = $this->singleBackend; + $this->doTestLockCalls(); + } + + private function doTestLockCalls() { + $backendName = $this->backendClass(); + + for ( $i=0; $i<50; $i++ ) { + $paths = array( + "test1.txt", + "test2.txt", + "test3.txt", + "subdir1", + "subdir1", // duplicate + "subdir1/test1.txt", + "subdir1/test2.txt", + "subdir2", + "subdir2", // duplicate + "subdir2/test3.txt", + "subdir2/test4.txt", + "subdir2/subdir", + "subdir2/subdir/test1.txt", + "subdir2/subdir/test2.txt", + "subdir2/subdir/test3.txt", + "subdir2/subdir/test4.txt", + "subdir2/subdir/test5.txt", + "subdir2/subdir/sub", + "subdir2/subdir/sub/test0.txt", + "subdir2/subdir/sub/120-px-file.txt", + ); + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + } + } + // test helper wrapper for backend prepare() function private function prepare( array $params ) { - $this->dirsToPrune[] = $params['dir']; return $this->backend->prepare( $params ); } + // test helper wrapper for backend prepare() function + private function create( array $params ) { + $params['op'] = 'create'; + return $this->backend->doQuickOperations( array( $params ) ); + } + function tearDownFiles() { foreach ( $this->filesToPrune as $file ) { @unlink( $file ); @@ -1328,10 +1836,7 @@ class FileBackendTest extends MediaWikiTestCase { foreach ( $containers as $container ) { $this->deleteFiles( $container ); } - foreach ( $this->dirsToPrune as $dir ) { - $this->recursiveClean( $dir ); - } - $this->filesToPrune = $this->dirsToPrune = array(); + $this->filesToPrune = array(); } private function deleteFiles( $container ) { @@ -1339,17 +1844,22 @@ class FileBackendTest extends MediaWikiTestCase { $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) ); if ( $iter ) { foreach ( $iter as $file ) { - $this->backend->delete( array( 'src' => "$base/$container/$file" ), array( 'force' => 1 ) ); + $this->backend->delete( array( 'src' => "$base/$container/$file" ), + array( 'force' => 1, 'nonLocking' => 1 ) ); } } + $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) ); } - private function recursiveClean( $dir ) { - do { - if ( !$this->backend->clean( array( 'dir' => $dir ) )->isOK() ) { - break; - } - } while ( $dir = FileBackend::parentStoragePath( $dir ) ); + function assertBackendPathsConsistent( array $paths ) { + if ( $this->backend instanceof FileBackendMultiWrite ) { + $status = $this->backend->consistencyCheck( $paths ); + $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) ); + } + } + + function assertGoodStatus( $status, $msg ) { + $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg ); } function tearDown() { diff --git a/tests/phpunit/includes/filerepo/FileRepoTest.php b/tests/phpunit/includes/filerepo/FileRepoTest.php index 0f023138..8f92c123 100644 --- a/tests/phpunit/includes/filerepo/FileRepoTest.php +++ b/tests/phpunit/includes/filerepo/FileRepoTest.php @@ -34,8 +34,12 @@ class FileRepoTest extends MediaWikiTestCase { function testFileRepoConstructionWithRequiredOptions() { $f = new FileRepo( array( 'name' => 'FileRepoTestRepository', - 'backend' => 'local-backend', - )); + 'backend' => new FSFileBackend( array( + 'name' => 'local-testing', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array() + ) ) + ) ); $this->assertInstanceOf( 'FileRepo', $f ); } } diff --git a/tests/phpunit/includes/filerepo/StoreBatchTest.php b/tests/phpunit/includes/filerepo/StoreBatchTest.php index 6abceeb3..3ab56af8 100644 --- a/tests/phpunit/includes/filerepo/StoreBatchTest.php +++ b/tests/phpunit/includes/filerepo/StoreBatchTest.php @@ -1,6 +1,7 @@ <?php /** * @group FileRepo + * @group medium */ class StoreBatchTest extends MediaWikiTestCase { diff --git a/tests/phpunit/includes/libs/CSSJanusTest.php b/tests/phpunit/includes/libs/CSSJanusTest.php new file mode 100644 index 00000000..54f66077 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSJanusTest.php @@ -0,0 +1,560 @@ +<?php +/** + * Based on the test suite of the original Python + * CSSJanus libary: + * http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus_test.py + * Ported to PHP for ResourceLoader and has been extended since. + */ +class CSSJanusTest extends MediaWikiTestCase { + /** + * @dataProvider provideTransformCases + */ + function testTransform( $cssA, $cssB = null ) { + + if ( $cssB ) { + $transformedA = CSSJanus::transform( $cssA ); + $this->assertEquals( $transformedA, $cssB, 'Test A-B transformation' ); + + $transformedB = CSSJanus::transform( $cssB ); + $this->assertEquals( $transformedB, $cssA, 'Test B-A transformation' ); + + // If no B version is provided, it means + // the output should equal the input. + } else { + $transformedA = CSSJanus::transform( $cssA ); + $this->assertEquals( $transformedA, $cssA, 'Nothing was flipped' ); + } + } + + /** + * @dataProvider provideTransformAdvancedCases + */ + function testTransformAdvanced( $code, $expectedOutput, $options = array() ) { + $swapLtrRtlInURL = isset( $options['swapLtrRtlInURL'] ) ? $options['swapLtrRtlInURL'] : false; + $swapLeftRightInURL = isset( $options['swapLeftRightInURL'] ) ? $options['swapLeftRightInURL'] : false; + + $flipped = CSSJanus::transform( $code, $swapLtrRtlInURL, $swapLeftRightInURL ); + + $this->assertEquals( $expectedOutput, $flipped, + 'Test flipping, options: url-ltr-rtl=' . ($swapLtrRtlInURL ? 'true' : 'false') + . ' url-left-right=' . ($swapLeftRightInURL ? 'true' : 'false') + ); + } + /** + * @dataProvider provideTransformBrokenCases + * @group Broken + */ + function testTransformBroken( $code, $expectedOutput ) { + $flipped = CSSJanus::transform( $code ); + + $this->assertEquals( $expectedOutput, $flipped, 'Test flipping' ); + } + + /** + * These transform cases are tested *in both directions* + * No need to declare a principle twice in both directions here. + */ + function provideTransformCases() { + return array( + // Property keys + array( + '.foo { left: 0; }', + '.foo { right: 0; }' + ), + // Guard against partial keys + // (CSS currently doesn't have flippable properties + // that contain the direction as part of the key without + // dash separation) + array( + '.foo { alright: 0; }' + ), + array( + '.foo { balleft: 0; }' + ), + + // Dashed property keys + array( + '.foo { padding-left: 0; }', + '.foo { padding-right: 0; }' + ), + array( + '.foo { margin-left: 0; }', + '.foo { margin-right: 0; }' + ), + array( + '.foo { border-left: 0; }', + '.foo { border-right: 0; }' + ), + + // Double-dashed property keys + array( + '.foo { border-left-color: red; }', + '.foo { border-right-color: red; }' + ), + array( + // Includes unknown properties? + '.foo { x-left-y: 0; }', + '.foo { x-right-y: 0; }' + ), + + // Multi-value properties + array( + '.foo { padding: 0; }' + ), + array( + '.foo { padding: 0 1px; }' + ), + array( + '.foo { padding: 0 1px 2px; }' + ), + array( + '.foo { padding: 0 1px 2px 3px; }', + '.foo { padding: 0 3px 2px 1px; }' + ), + + // Shorthand / Four notation + array( + '.foo { padding: .25em 15px 0pt 0ex; }', + '.foo { padding: .25em 0ex 0pt 15px; }' + ), + array( + '.foo { margin: 1px -4px 3px 2px; }', + '.foo { margin: 1px 2px 3px -4px; }' + ), + array( + '.foo { padding: 0 15px .25em 0; }', + '.foo { padding: 0 0 .25em 15px; }' + ), + array( + '.foo { padding: 1px 4.1grad 3px 2%; }', + '.foo { padding: 1px 2% 3px 4.1grad; }' + ), + array( + '.foo { padding: 1px 2px 3px auto; }', + '.foo { padding: 1px auto 3px 2px; }' + ), + array( + '.foo { padding: 1px inherit 3px auto; }', + '.foo { padding: 1px auto 3px inherit; }' + ), + array( + '.foo { border-radius: .25em 15px 0pt 0ex; }', + '.foo { border-radius: .25em 0ex 0pt 15px; }' + ), + array( + '.foo { x-unknown: a b c d; }' + ), + array( + '.foo barpx 0 2% { opacity: 0; }' + ), + array( + '#settings td p strong' + ), + array( + # Not sure how 4+ values should behave, + # testing to make sure changes are detected + '.foo { x-unknown: 1 2 3 4 5; }', + '.foo { x-unknown: 1 4 3 2 5; }', + ), + array( + '.foo { x-unknown: 1 2 3 4 5 6; }', + '.foo { x-unknown: 1 4 3 2 5 6; }', + ), + + // Shorthand / Three notation + array( + '.foo { margin: 1em 0 .25em; }' + ), + array( + '.foo { margin:-1.5em 0 -.75em; }' + ), + + // Shorthand / Two notation + array( + '.foo { padding: 1px 2px; }' + ), + + // Shorthand / One notation + array( + '.foo { padding: 1px; }' + ), + + // Direction + // Note: This differs from the Python implementation, + // see also CSSJanus::fixDirection for more info. + array( + '.foo { direction: ltr; }', + '.foo { direction: rtl; }' + ), + array( + '.foo { direction: rtl; }', + '.foo { direction: ltr; }' + ), + array( + 'input { direction: ltr; }', + 'input { direction: rtl; }' + ), + array( + 'input { direction: rtl; }', + 'input { direction: ltr; }' + ), + array( + 'body { direction: ltr; }', + 'body { direction: rtl; }' + ), + array( + '.foo, body, input { direction: ltr; }', + '.foo, body, input { direction: rtl; }' + ), + array( + 'body { padding: 10px; direction: ltr; }', + 'body { padding: 10px; direction: rtl; }' + ), + array( + 'body { direction: ltr } .myClass { direction: ltr }', + 'body { direction: rtl } .myClass { direction: rtl }' + ), + + // Left/right values + array( + '.foo { float: left; }', + '.foo { float: right; }' + ), + array( + '.foo { text-align: left; }', + '.foo { text-align: right; }' + ), + array( + '.foo { -x-unknown: left; }', + '.foo { -x-unknown: right; }' + ), + // Guard against selectors that look flippable + array( + '.column-left { width: 0; }' + ), + array( + 'a.left { width: 0; }' + ), + array( + 'a.leftification { width: 0; }' + ), + array( + 'a.ltr { width: 0; }' + ), + array( + # <div class="a-ltr png"> + '.a-ltr.png { width: 0; }' + ), + array( + # <foo-ltr attr="x"> + 'foo-ltr[attr="x"] { width: 0; }' + ), + array( + 'div.left > span.right+span.left { width: 0; }' + ), + array( + '.thisclass .left .myclass { width: 0; }' + ), + array( + '.thisclass .left .myclass #myid { width: 0; }' + ), + + // Cursor values (east/west) + array( + '.foo { cursor: e-resize; }', + '.foo { cursor: w-resize; }' + ), + array( + '.foo { cursor: se-resize; }', + '.foo { cursor: sw-resize; }' + ), + array( + '.foo { cursor: ne-resize; }', + '.foo { cursor: nw-resize; }' + ), + + // Background + array( + '.foo { background-position: top left; }', + '.foo { background-position: top right; }' + ), + array( + '.foo { background: url(/foo/bar.png) top left; }', + '.foo { background: url(/foo/bar.png) top right; }' + ), + array( + '.foo { background: url(/foo/bar.png) top left no-repeat; }', + '.foo { background: url(/foo/bar.png) top right no-repeat; }' + ), + array( + '.foo { background: url(/foo/bar.png) no-repeat top left; }', + '.foo { background: url(/foo/bar.png) no-repeat top right; }' + ), + array( + '.foo { background: #fff url(/foo/bar.png) no-repeat top left; }', + '.foo { background: #fff url(/foo/bar.png) no-repeat top right; }' + ), + array( + '.foo { background-position: 100% 40%; }', + '.foo { background-position: 0% 40%; }' + ), + array( + '.foo { background-position: 23% 0; }', + '.foo { background-position: 77% 0; }' + ), + array( + '.foo { background-position: 23% auto; }', + '.foo { background-position: 77% auto; }' + ), + array( + '.foo { background-position-x: 23%; }', + '.foo { background-position-x: 77%; }' + ), + array( + '.foo { background-position-y: 23%; }', + '.foo { background-position-y: 23%; }' + ), + array( + '.foo { background:url(../foo.png) no-repeat 75% 50%; }', + '.foo { background:url(../foo.png) no-repeat 25% 50%; }' + ), + array( + '.foo { background: 10% 20% } .bar { background: 40% 30% }', + '.foo { background: 90% 20% } .bar { background: 60% 30% }' + ), + + // Multiple rules + array( + 'body { direction: rtl; float: right; } .foo { direction: ltr; float: right; }', + 'body { direction: ltr; float: left; } .foo { direction: rtl; float: left; }', + ), + + // Duplicate properties + array( + '.foo { float: left; float: right; float: left; }', + '.foo { float: right; float: left; float: right; }', + ), + + // Preserve comments + array( + '/* left /* right */left: 10px', + '/* left /* right */right: 10px' + ), + array( + '/*left*//*left*/left: 10px', + '/*left*//*left*/right: 10px' + ), + array( + '/* Going right is cool */ .foo { width: 0 }', + ), + array( + "/* padding-right 1 2 3 4 */\n#test { width: 0}\n/*right*/" + ), + array( + "/** Two line comment\n * left\n \*/\n#test {width: 0}" + ), + + // @noflip annotation + array( + // before selector (single) + '/* @noflip */ div { float: left; }' + ), + array( + // before selector (multiple) + '/* @noflip */ div, .notme { float: left; }' + ), + array( + // inside selector + 'div, /* @noflip */ .foo { float: left; }' + ), + array( + // after selector + 'div, .notme /* @noflip */ { float: left; }' + ), + array( + // before multiple rules + '/* @noflip */ div { float: left; } .foo { float: left; }', + '/* @noflip */ div { float: left; } .foo { float: right; }' + ), + array( + // after multiple rules + '.foo { float: left; } /* @noflip */ div { float: left; }', + '.foo { float: right; } /* @noflip */ div { float: left; }' + ), + array( + // before multiple properties + 'div { /* @noflip */ float: left; text-align: left; }', + 'div { /* @noflip */ float: left; text-align: right; }' + ), + array( + // after multiple properties + 'div { float: left; /* @noflip */ text-align: left; }', + 'div { float: right; /* @noflip */ text-align: left; }' + ), + + // Guard against css3 stuff + array( + 'background-image: -moz-linear-gradient(#326cc1, #234e8c);' + ), + array( + 'background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, from(#666666), to(#ffffff));' + ), + + // CSS syntax / white-space variations + // spaces, no spaces, tabs, new lines, omitting semi-colons + array( + ".foo { left: 0; }", + ".foo { right: 0; }" + ), + array( + ".foo{ left: 0; }", + ".foo{ right: 0; }" + ), + array( + ".foo{ left: 0 }", + ".foo{ right: 0 }" + ), + array( + ".foo{left:0 }", + ".foo{right:0 }" + ), + array( + ".foo{left:0}", + ".foo{right:0}" + ), + array( + ".foo { left : 0 ; }", + ".foo { right : 0 ; }" + ), + array( + ".foo\n { left : 0 ; }", + ".foo\n { right : 0 ; }" + ), + array( + ".foo\n { \nleft : 0 ; }", + ".foo\n { \nright : 0 ; }" + ), + array( + ".foo\n { \n left : 0 ; }", + ".foo\n { \n right : 0 ; }" + ), + array( + ".foo\n { \n left\n : 0; }", + ".foo\n { \n right\n : 0; }" + ), + array( + ".foo \n { \n left\n : 0; }", + ".foo \n { \n right\n : 0; }" + ), + array( + ".foo\n{\nleft\n:\n0;}", + ".foo\n{\nright\n:\n0;}" + ), + array( + ".foo\n.bar {\n\tleft: 0;\n}", + ".foo\n.bar {\n\tright: 0;\n}" + ), + array( + ".foo\t{\tleft\t:\t0;}", + ".foo\t{\tright\t:\t0;}" + ), + ); + } + + /** + * These cases are tested in one way only (format: actual, expected, msg). + * If both ways can be tested, either put both versions in here or move + * it to provideTransformCases(). + */ + function provideTransformAdvancedCases() { + $bgPairs = array( + # [ - _ . ] <-> [ left right ltr rtl ] + 'foo.jpg' => 'foo.jpg', + 'left.jpg' => 'right.jpg', + 'ltr.jpg' => 'rtl.jpg', + + 'foo-left.png' => 'foo-right.png', + 'foo_left.png' => 'foo_right.png', + 'foo.left.png' => 'foo.right.png', + + 'foo-ltr.png' => 'foo-rtl.png', + 'foo_ltr.png' => 'foo_rtl.png', + 'foo.ltr.png' => 'foo.rtl.png', + + 'left-foo.png' => 'right-foo.png', + 'left_foo.png' => 'right_foo.png', + 'left.foo.png' => 'right.foo.png', + + 'ltr-foo.png' => 'rtl-foo.png', + 'ltr_foo.png' => 'rtl_foo.png', + 'ltr.foo.png' => 'rtl.foo.png', + + 'foo-ltr-left.gif' => 'foo-rtl-right.gif', + 'foo_ltr_left.gif' => 'foo_rtl_right.gif', + 'foo.ltr.left.gif' => 'foo.rtl.right.gif', + 'foo-ltr_left.gif' => 'foo-rtl_right.gif', + 'foo_ltr.left.gif' => 'foo_rtl.right.gif', + ); + $provider = array(); + foreach ( $bgPairs as $left => $right ) { + # By default '-rtl' and '-left' etc. are not touched, + # Only when the appropiate parameter is set. + $provider[] = array( + ".foo { background: url(images/$left); }", + ".foo { background: url(images/$left); }" + ); + $provider[] = array( + ".foo { background: url(images/$right); }", + ".foo { background: url(images/$right); }" + ); + $provider[] = array( + ".foo { background: url(images/$left); }", + ".foo { background: url(images/$right); }", + array( + 'swapLtrRtlInURL' => true, + 'swapLeftRightInURL' => true, + ) + ); + $provider[] = array( + ".foo { background: url(images/$right); }", + ".foo { background: url(images/$left); }", + array( + 'swapLtrRtlInURL' => true, + 'swapLeftRightInURL' => true, + ) + ); + } + + return $provider; + } + + /** + * Cases that are currently failing, but + * should be looked at in the future as enhancements and/or bug fix + */ + function provideTransformBrokenCases() { + return array( + // Guard against partial keys + array( + '.foo { leftxx: 0; }', + '.foo { leftxx: 0; }' + ), + array( + '.foo { rightxx: 0; }', + '.foo { rightxx: 0; }' + ), + + // Guard against selectors that look flippable + array( + # <foo-left-x attr="x"> + 'foo-left-x[attr="x"] { width: 0; }', + 'foo-left-x[attr="x"] { width: 0; }' + ), + array( + # <div class="foo" data-left="x"> + '.foo[data-left="x"] { width: 0; }', + '.foo[data-left="x"] { width: 0; }' + ), + ); + } +} diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php new file mode 100644 index 00000000..a3827756 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -0,0 +1,142 @@ +<?php +/** + * This file test the CSSMin library shipped with Mediawiki. + * + * @author Timo Tijhof + */ + +class CSSMinTest extends MediaWikiTestCase { + protected $oldServer = null, $oldCanServer = null; + + function setUp() { + parent::setUp(); + + // Fake $wgServer and $wgCanonicalServer + global $wgServer, $wgCanonicalServer; + $this->oldServer = $wgServer; + $this->oldCanServer = $wgCanonicalServer; + $wgServer = $wgCanonicalServer = 'http://wiki.example.org'; + } + + function tearDown() { + // Restore $wgServer and $wgCanonicalServer + global $wgServer, $wgCanonicalServer; + $wgServer = $this->oldServer; + $wgCanonicalServer = $this->oldCanServer; + + parent::tearDown(); + } + + /** + * @dataProvider provideMinifyCases + */ + function testMinify( $code, $expectedOutput ) { + $minified = CSSMin::minify( $code ); + + $this->assertEquals( $expectedOutput, $minified, 'Minified output should be in the form expected.' ); + } + + function provideMinifyCases() { + return array( + // Whitespace + array( "\r\t\f \v\n\r", "" ), + array( "foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + + // Loose comments + array( "/* foo */", "" ), + array( "/*******\n foo\n *******/", "" ), + array( "/*!\n foo\n */", "" ), + + // Inline comments in various different places + array( "/* comment */foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo/* comment */, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo,/* comment */ bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar/* comment */ {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\t/* comment */prop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\tprop: /* comment */value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\tprop: value /* comment */;\n}", "foo,bar{prop:value }" ), + array( "foo, bar {\n\tprop: value; /* comment */\n}", "foo,bar{prop:value; }" ), + + // Keep track of things that aren't as minified as much as they + // could be (bug 35493) + array( 'foo { prop: value ;}', 'foo{prop:value }' ), + array( 'foo { prop : value; }', 'foo{prop :value}' ), + array( 'foo { prop: value ; }', 'foo{prop:value }' ), + array( 'foo { font-family: "foo" , "bar"; }', 'foo{font-family:"foo" ,"bar"}' ), + array( "foo { src:\n\turl('foo') ,\n\turl('bar') ; }", "foo{src:url('foo') ,url('bar') }" ), + + // Interesting cases with string values + // - Double quotes, single quotes + array( 'foo { content: ""; }', 'foo{content:""}' ), + array( "foo { content: ''; }", "foo{content:''}" ), + array( 'foo { content: "\'"; }', 'foo{content:"\'"}' ), + array( "foo { content: '\"'; }", "foo{content:'\"'}" ), + // - Whitespace in string values + array( 'foo { content: " "; }', 'foo{content:" "}' ), + ); + } + + /** + * @dataProvider provideRemapCases + */ + function testRemap( $message, $params, $expectedOutput ) { + $remapped = call_user_func_array( 'CSSMin::remap', $params ); + + $messageAdd = " Case: $message"; + $this->assertEquals( $expectedOutput, $remapped, 'CSSMin::remap should return the expected url form.' . $messageAdd ); + } + + function provideRemapCases() { + // Parameter signature: + // CSSMin::remap( $code, $local, $remote, $embedData = true ) + return array( + array( + 'Simple case', + array( 'foo { prop: url(bar.png); }', false, 'http://example.org', false ), + 'foo { prop: url(http://example.org/bar.png); }', + ), + array( + 'Without trailing slash', + array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux', false ), + 'foo { prop: url(http://example.org/quux/../bar.png); }', + ), + array( + 'With trailing slash on remote (bug 27052)', + array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux/', false ), + 'foo { prop: url(http://example.org/quux/../bar.png); }', + ), + array( + 'Guard against stripping double slashes from query', + array( 'foo { prop: url(bar.png?corge=//grault); }', false, 'http://example.org/quux/', false ), + 'foo { prop: url(http://example.org/quux/bar.png?corge=//grault); }', + ), + array( + 'Expand absolute paths', + array( 'foo { prop: url(/w/skin/images/bar.png); }', false, 'http://example.org/quux', false ), + 'foo { prop: url(http://wiki.example.org/w/skin/images/bar.png); }', + ), + ); + } + + /** + * Seperated because they are currently broken (bug 35492) + * + * @group Broken + * @dataProvider provideStringCases + */ + function testMinifyWithCSSStringValues( $code, $expectedOutput ) { + $this->testMinifyOutput( $code, $expectedOutput ); + } + + function provideStringCases() { + return array( + // String values should be respected + // - More than one space in a string value + array( 'foo { content: " "; }', 'foo{content:" "}' ), + // - Using a tab in a string value (turns into a space) + array( "foo { content: '\t'; }", "foo{content:'\t'}" ), + // - Using css-like syntax in string values + array( 'foo::after { content: "{;}"; position: absolute; }', 'foo::after{content:"{;}";position:absolute}' ), + ); + } +} diff --git a/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/includes/libs/GenericArrayObjectTest.php new file mode 100644 index 00000000..70fce111 --- /dev/null +++ b/tests/phpunit/includes/libs/GenericArrayObjectTest.php @@ -0,0 +1,245 @@ +<?php + +/** + * Tests for the GenericArrayObject and deriving classes. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.20 + * + * @ingroup Test + * @group GenericArrayObject + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class GenericArrayObjectTest extends MediaWikiTestCase { + + /** + * Returns objects that can serve as elements in the concrete GenericArrayObject deriving class being tested. + * + * @since 1.20 + * + * @return array + */ + public abstract function elementInstancesProvider(); + + /** + * Returns the name of the concrete class being tested. + * + * @since 1.20 + * + * @return string + */ + public abstract function getInstanceClass(); + + /** + * Provides instances of the concrete class being tested. + * + * @since 1.20 + * + * @return array + */ + public function instanceProvider() { + $instances = array(); + + foreach ( $this->elementInstancesProvider() as $elementInstances ) { + $instances[] = $this->getNew( $elementInstances ); + } + + return $this->arrayWrap( $instances ); + } + + /** + * @since 1.20 + * + * @param array $elements + * + * @return GenericArrayObject + */ + protected function getNew( array $elements = array() ) { + $class = $this->getInstanceClass(); + return new $class( $elements ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testConstructor( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( count( $elements ), $arrayObject->count() ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testIsEmpty( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( $elements === array(), $arrayObject->isEmpty() ); + } + + /** + * @dataProvider instanceProvider + * + * @since 1.20 + * + * @param GenericArrayObject $list + */ + public function testUnset( GenericArrayObject $list ) { + if ( !$list->isEmpty() ) { + $offset = $list->getIterator()->key(); + $count = $list->count(); + $list->offsetUnset( $offset ); + $this->assertEquals( $count - 1, $list->count() ); + } + + if ( !$list->isEmpty() ) { + $offset = $list->getIterator()->key(); + $count = $list->count(); + unset( $list[$offset] ); + $this->assertEquals( $count - 1, $list->count() ); + } + + $exception = null; + try { $list->offsetUnset( 'sdfsedtgsrdysftu' ); } catch ( \Exception $exception ){} + $this->assertInstanceOf( '\Exception', $exception ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testAppend( array $elements ) { + $list = $this->getNew(); + + $listSize = count( $elements ); + + foreach ( $elements as $element ) { + $list->append( $element ); + } + + $this->assertEquals( $listSize, $list->count() ); + + $list = $this->getNew(); + + foreach ( $elements as $element ) { + $list[] = $element; + } + + $this->assertEquals( $listSize, $list->count() ); + + $this->checkTypeChecks( function( GenericArrayObject $list, $element ) { + $list->append( $element ); + } ); + } + + /** + * @since 1.20 + * + * @param callback $function + */ + protected function checkTypeChecks( $function ) { + $excption = null; + $list = $this->getNew(); + + $elementClass = $list->getObjectType(); + + foreach ( array( 42, 'foo', array(), new \stdClass(), 4.2 ) as $element ) { + $validValid = $element instanceof $elementClass; + + try{ + call_user_func( $function, $list, $element ); + $valid = true; + } + catch ( InvalidArgumentException $exception ) { + $valid = false; + } + + $this->assertEquals( + $validValid, + $valid, + 'Object of invalid type got successfully added to a GenericArrayObject' + ); + } + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testOffsetSet( array $elements ) { + if ( $elements === array() ) { + $this->assertTrue( true ); + return; + } + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( 42, $element ); + $this->assertEquals( $element, $list->offsetGet( 42 ) ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list['oHai'] = $element; + $this->assertEquals( $element, $list['oHai'] ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( 9001, $element ); + $this->assertEquals( $element, $list[9001] ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( null, $element ); + $this->assertEquals( $element, $list[0] ); + + $list = $this->getNew(); + $offset = 0; + + foreach ( $elements as $element ) { + $list->offsetSet( null, $element ); + $this->assertEquals( $element, $list[$offset++] ); + } + + $this->assertEquals( count( $elements ), $list->count() ); + + $this->checkTypeChecks( function( GenericArrayObject $list, $element ) { + $list->offsetSet( mt_rand(), $element ); + } ); + } + +} diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php index d2bfeedf..f121b018 100644 --- a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php +++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php @@ -4,9 +4,18 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { function provideCases() { return array( - // Basic tokens + + // Basic whitespace and comments that should be stripped entirely array( "\r\t\f \v\n\r", "" ), array( "/* Foo *\n*bar\n*/", "" ), + + /** + * Slashes used inside block comments (bug 26931). + * At some point there was a bug that caused this comment to be ended at '* /', + * causing /M... to be left as the beginning of a regex. + */ + array( "/**\n * Foo\n * {\n * 'bar' : {\n * //Multiple rules with configurable operators\n * 'baz' : false\n * }\n */", ""), + /** * ' Foo \' bar \ * baz \' quox ' . @@ -15,11 +24,13 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { array( "\" Foo \\\" bar \\\n baz \\\" quox \" .length", "\" Foo \\\" bar \\\n baz \\\" quox \".length" ), array( "// Foo b/ar baz", "" ), array( "/ Foo \\/ bar [ / \\] / ] baz / .length", "/ Foo \\/ bar [ / \\] / ] baz /.length" ), + // HTML comments array( "<!-- Foo bar", "" ), array( "<!-- Foo --> bar", "" ), array( "--> Foo", "" ), array( "x --> y", "x-->y" ), + // Semicolon insertion array( "(function(){return\nx;})", "(function(){return\nx;})" ), array( "throw\nx;", "throw\nx;" ), @@ -35,12 +46,19 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { array( "5.\nx;", "5.\nx;" ), array( "0xFF.\nx;", "0xFF.x;" ), array( "5.3.\nx;", "5.3.x;" ), + + // Semicolon insertion between an expression having an inline + // comment after it, and a statement on the next line (bug 27046). + array( "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}", "var a=this\nfor(b=0;c<d;b++){}" ), + // Token separation array( "x in y", "x in y" ), array( "/x/g in y", "/x/g in y" ), array( "x in 30", "x in 30" ), array( "x + ++ y", "x+ ++y" ), + array( "x ++ + y", "x++ +y" ), array( "x / /y/.exec(z)", "x/ /y/.exec(z)" ), + // State machine array( "/ x/g", "/ x/g" ), array( "(function(){return/ x/g})", "(function(){return/ x/g})" ), @@ -63,15 +81,18 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { array( "function x(){}/ x/g", "function x(){}/ x/g" ), array( "+function x(){}/ x/g", "+function x(){}/x/g" ), - // Tests for things that broke in the past // Multiline quoted string array( "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ), + // Multiline quoted string followed by string with spaces array( "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n", "var foo=\"\\\nblah\\\n\";var baz=\" foo \";" ), + // URL in quoted string ( // is not a comment) array( "aNode.setAttribute('href','http://foo.bar.org/baz');", "aNode.setAttribute('href','http://foo.bar.org/baz');" ), + // URL in quoted string after multiline quoted string array( "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');", "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');" ), + // Division vs. regex nastiness array( "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );", "alert((10+10)/'/'.charCodeAt(0)+'//');" ), array( "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ), @@ -81,11 +102,12 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { // Unicode letter characters should pass through ok in identifiers (bug 31187) array( "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}'), - // And also per spec unicode char escape values should work in identifiers, + + // Per spec unicode char escape values should work in identifiers, // as long as it's a valid char. In future it might get normalized. array( "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}'), - /* Some structures that might look invalid at first sight */ + // Some structures that might look invalid at first sight array( "var a = 5.;", "var a=5.;" ), array( "5.0.toString();", "5.0.toString();" ), array( "5..toString();", "5..toString();" ), @@ -110,24 +132,6 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { $this->assertEquals( $expectedOutput, $minified, "Minified output should be in the form expected." ); } - /** - * @dataProvider provideBug32548 - */ - function testBug32548Exponent($num) { - // Long line breaking was being incorrectly done between the base and - // exponent part of a number, causing a syntax error. The line should - // instead break at the start of the number. - $prefix = 'var longVarName' . str_repeat('_', 973) . '='; - $suffix = ',shortVarName=0;'; - - $input = $prefix . $num . $suffix; - $expected = $prefix . "\n" . $num . $suffix; - - $minified = JavaScriptMinifier::minify( $input ); - - $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent"); - } - function provideBug32548() { return array( array( @@ -145,4 +149,22 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { ), ); } + + /** + * @dataProvider provideBug32548 + */ + function testBug32548Exponent( $num ) { + // Long line breaking was being incorrectly done between the base and + // exponent part of a number, causing a syntax error. The line should + // instead break at the start of the number. + $prefix = 'var longVarName' . str_repeat( '_', 973 ) . '='; + $suffix = ',shortVarName=0;'; + + $input = $prefix . $num . $suffix; + $expected = $prefix . "\n" . $num . $suffix; + + $minified = JavaScriptMinifier::minify( $input ); + + $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent"); + } } diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php index f4f52dd8..88f87ef9 100644 --- a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php +++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php @@ -2,7 +2,7 @@ class BitmapMetadataHandlerTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; } /** @@ -73,7 +73,8 @@ class BitmapMetadataHandlerTest extends MediaWikiTestCase { $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] ); $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] ); } - /* File has an invalid time (+ one valid but really weird time) + /** + * File has an invalid time (+ one valid but really weird time) * that shouldn't be included */ public function testIPTCDatesInvalid() { diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php index 25149a05..6af52dd1 100644 --- a/tests/phpunit/includes/media/ExifRotationTest.php +++ b/tests/phpunit/includes/media/ExifRotationTest.php @@ -5,16 +5,12 @@ */ class ExifRotationTest extends MediaWikiTestCase { - /** track directories creations. Content will be deleted. */ - private $createdDirs = array(); - function setUp() { parent::setUp(); $this->handler = new BitmapHandler(); - $filePath = dirname( __FILE__ ) . '/../../data/media'; + $filePath = __DIR__ . '/../../data/media'; - $tmpDir = wfTempDir() . '/exif-test-' . time() . '-' . mt_rand(); - $this->createdDirs[] = $tmpDir; + $tmpDir = $this->getNewTempDirectory(); $this->repo = new FSRepo( array( 'name' => 'temp', @@ -42,17 +38,7 @@ class ExifRotationTest extends MediaWikiTestCase { $wgShowEXIF = $this->show; $wgEnableAutoRotation = $this->oldAuto; - $this->tearDownFiles(); - } - - private function tearDownFiles() { - foreach( $this->createdDirs as $dir ) { - wfRecursiveRemoveDir( $dir ); - } - } - - function __destruct() { - $this->tearDownFiles(); + parent::tearDown(); } /** diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php index b39c15e4..045777d7 100644 --- a/tests/phpunit/includes/media/ExifTest.php +++ b/tests/phpunit/includes/media/ExifTest.php @@ -2,22 +2,22 @@ class ExifTest extends MediaWikiTestCase { public function setUp() { - $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/'; + $this->mediaPath = __DIR__ . '/../../data/media/'; if ( !wfDl( 'exif' ) ) { $this->markTestSkipped( "This test needs the exif extension." ); } - global $wgShowEXIF; - $this->showExif = $wgShowEXIF; - $wgShowEXIF = true; + global $wgShowEXIF; + $this->showExif = $wgShowEXIF; + $wgShowEXIF = true; } - public function tearDown() { - global $wgShowEXIF; - $wgShowEXIF = $this->showExif; - } - public function testGPSExtraction() { + public function tearDown() { + global $wgShowEXIF; + $wgShowEXIF = $this->showExif; + } + public function testGPSExtraction() { $filename = $this->mediaPath . 'exif-gps.jpg'; $seg = JpegMetadataExtractor::segmentSplitter( $filename ); $exif = new Exif( $filename, $seg['byteOrder'] ); @@ -25,14 +25,14 @@ class ExifTest extends MediaWikiTestCase { $expected = array( 'GPSLatitude' => 88.5180555556, 'GPSLongitude' => -21.12357, - 'GPSAltitude' => -200, + 'GPSAltitude' => -3.141592653, 'GPSDOP' => '5/1', 'GPSVersionID' => '2.2.0.0', ); $this->assertEquals( $expected, $data, '', 0.0000000001 ); } - public function testUnicodeUserComment() { + public function testUnicodeUserComment() { $filename = $this->mediaPath . 'exif-user-comment.jpg'; $seg = JpegMetadataExtractor::segmentSplitter( $filename ); $exif = new Exif( $filename, $seg['byteOrder'] ); diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php index 8a632f52..6ade6702 100644 --- a/tests/phpunit/includes/media/FormatMetadataTest.php +++ b/tests/phpunit/includes/media/FormatMetadataTest.php @@ -4,7 +4,7 @@ class FormatMetadataTest extends MediaWikiTestCase { if ( !wfDl( 'exif' ) ) { $this->markTestSkipped( "This test needs the exif extension." ); } - $filePath = dirname( __FILE__ ) . '/../../data/media'; + $filePath = __DIR__ . '/../../data/media'; $this->backend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'nullLockManager', diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php index 47fc368b..650fdd5c 100644 --- a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php @@ -2,7 +2,7 @@ class GIFMetadataExtractorTest extends MediaWikiTestCase { public function setUp() { - $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/'; + $this->mediaPath = __DIR__ . '/../../data/media/'; } /** * Put in a file, and see if the metadata coming out is as expected. diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php index 36658358..5dcbeee0 100644 --- a/tests/phpunit/includes/media/GIFTest.php +++ b/tests/phpunit/includes/media/GIFTest.php @@ -2,7 +2,7 @@ class GIFHandlerTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media'; + $this->filePath = __DIR__ . '/../../data/media'; $this->backend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'nullLockManager', diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php index f48382a4..41d81190 100644 --- a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -9,7 +9,7 @@ class JpegMetadataExtractorTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; } /** diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php index ddabf5b8..ea007f90 100644 --- a/tests/phpunit/includes/media/JpegTest.php +++ b/tests/phpunit/includes/media/JpegTest.php @@ -2,7 +2,7 @@ class JpegTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; if ( !wfDl( 'exif' ) ) { $this->markTestSkipped( "This test needs the exif extension." ); } diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php index 9f702c50..1b1b2ec3 100644 --- a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -2,7 +2,7 @@ class PNGMetadataExtractorTest extends MediaWikiTestCase { function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; } /** * Tests zTXt tag (compressed textual metadata) diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php index b6f911fd..fe73c9c7 100644 --- a/tests/phpunit/includes/media/PNGTest.php +++ b/tests/phpunit/includes/media/PNGTest.php @@ -2,7 +2,7 @@ class PNGHandlerTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media'; + $this->filePath = __DIR__ . '/../../data/media'; $this->backend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'nullLockManager', diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php index 526beae8..2116554e 100644 --- a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -39,27 +39,33 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { } function providerSvgFiles() { - $base = dirname( __FILE__ ) . '/../../data/media'; + $base = __DIR__ . '/../../data/media'; return array( array( "$base/Wikimedia-logo.svg", array( 'width' => 1024, - 'height' => 1024 + 'height' => 1024, + 'originalWidth' => '1024', + 'originalHeight' => '1024', ) ), array( "$base/QA_icon.svg", array( 'width' => 60, - 'height' => 60 + 'height' => 60, + 'originalWidth' => '60', + 'originalHeight' => '60', ) ), array( "$base/Gtk-media-play-ltr.svg", array( 'width' => 60, - 'height' => 60 + 'height' => 60, + 'originalWidth' => '60.0000000', + 'originalHeight' => '60.0000000', ) ), array( @@ -67,14 +73,16 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { // This file triggered bug 31719, needs entity expansion in the xmlns checks array( 'width' => 385, - 'height' => 385 + 'height' => 385, + 'originalWidth' => '385', + 'originalHeight' => '385.0004883', ) ) ); } function providerSvgFilesWithXMLMetadata() { - $base = dirname( __FILE__ ) . '/../../data/media'; + $base = __DIR__ . '/../../data/media'; $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about=""> @@ -89,7 +97,9 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { array( 'height' => 593, 'metadata' => $metadata, - 'width' => 959 + 'width' => 959, + 'originalWidth' => '958.69', + 'originalHeight' => '592.78998', ) ), ); diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php index d4cf503b..4c79f66c 100644 --- a/tests/phpunit/includes/media/TiffTest.php +++ b/tests/phpunit/includes/media/TiffTest.php @@ -5,7 +5,7 @@ class TiffTest extends MediaWikiTestCase { global $wgShowEXIF; $this->showExif = $wgShowEXIF; $wgShowEXIF = true; - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; $this->handler = new TiffHandler; } diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php index c952b23c..8198d3b0 100644 --- a/tests/phpunit/includes/media/XMPTest.php +++ b/tests/phpunit/includes/media/XMPTest.php @@ -22,11 +22,11 @@ class XMPTest extends MediaWikiTestCase { } $reader = new XMPReader; $reader->parse( $xmp ); - $this->assertEquals( $expected, $reader->getResults(), $info ); + $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 ); } public function dataXMPParse() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/' ; + $xmpPath = __DIR__ . '/../../data/xmp/' ; $data = array(); // $xmpFiles format: array of arrays with first arg file base name, @@ -52,6 +52,7 @@ class XMPTest extends MediaWikiTestCase { array( 'utf32BE', 'UTF-32BE encoding' ), array( 'utf32LE', 'UTF-32LE encoding' ), array( 'xmpExt', 'Extended XMP missing second part' ), + array( 'gps', 'Handling of exif GPS parameters in XMP' ), ); foreach( $xmpFiles as $file ) { $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' ); @@ -72,7 +73,7 @@ class XMPTest extends MediaWikiTestCase { * world example file to double check the support for this is right. */ function testExtendedXMP() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $xmpPath = __DIR__ . '/../../data/xmp/'; $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); @@ -102,7 +103,7 @@ class XMPTest extends MediaWikiTestCase { * and thus should only return the StandardXMP, not the ExtendedXMP. */ function testExtendedXMPWithWrongGUID() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $xmpPath = __DIR__ . '/../../data/xmp/'; $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); @@ -130,7 +131,7 @@ class XMPTest extends MediaWikiTestCase { * which should cause it to ignore the ExtendedXMP packet. */ function testExtendedXMPMissingPacket() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $xmpPath = __DIR__ . '/../../data/xmp/'; $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); diff --git a/tests/phpunit/includes/mobile/DeviceDetectionTest.php b/tests/phpunit/includes/mobile/DeviceDetectionTest.php new file mode 100644 index 00000000..0e156532 --- /dev/null +++ b/tests/phpunit/includes/mobile/DeviceDetectionTest.php @@ -0,0 +1,40 @@ +<?php + +/** + * @group Mobile + */ + class DeviceDetectionTest extends MediaWikiTestCase { + + /** + * @dataProvider provideTestFormatName + */ + public function testFormatName( $format, $userAgent ) { + $detector = new DeviceDetection(); + $this->assertEquals( $format, $detector->detectFormatName( $userAgent ) ); + } + + public function provideTestFormatName() { + return array( + array( 'android', 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17' ), + array( 'iphone2', 'Mozilla/5.0 (ipod: U;CPU iPhone OS 2_2 like Mac OS X: es_es) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3' ), + array( 'iphone', 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3' ), + array( 'nokia', 'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413' ), + array( 'palm_pre', 'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0' ), + array( 'wii', 'Opera/9.00 (Nintendo Wii; U; ; 1309-9; en)' ), + array( 'operamini', 'Opera/9.50 (J2ME/MIDP; Opera Mini/4.0.10031/298; U; en)' ), + array( 'operamobile', 'Opera/9.51 Beta (Microsoft Windows; PPC; Opera Mobi/1718; U; en)' ), + array( 'kindle', 'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)' ), + array( 'kindle2', 'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)' ), + array( 'capable', 'Mozilla/5.0 (X11; Linux i686; rv:2.0.1) Gecko/20100101 Firefox/4.0.1' ), + array( 'netfront', 'Mozilla/4.08 (Windows; Mobile Content Viewer/1.0) NetFront/3.2' ), + array( 'wap2', 'SonyEricssonK608i/R2L/SN356841000828910 Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration/CLDC-1.1' ), + array( 'wap2', 'NokiaN73-2/3.0-630.0.2 Series60/3.0 Profile/MIDP-2.0 Configuration/CLDC-1.1' ), + array( 'psp', 'Mozilla/4.0 (PSP (PlayStation Portable); 2.00)' ), + array( 'ps3', 'Mozilla/5.0 (PLAYSTATION 3; 1.00)' ), + array( 'ie', 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)' ), + array( 'ie', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)' ), + array( 'blackberry', 'BlackBerry9300/5.0.0.716 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/133' ), + array( 'blackberry-lt5', 'BlackBerry7250/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1' ), + ); + } +} diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index 816c017a..6a6fded1 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -1,5 +1,5 @@ <?php -require_once( dirname( __FILE__ ) . '/NewParserTest.php' ); +require_once( __DIR__ . '/NewParserTest.php' ); /** * The UnitTest must be either a class that inherits from MediaWikiTestCase diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index d9b16710..69a96e66 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -186,7 +186,7 @@ class NewParserTest extends MediaWikiTestCase { if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { $image->recordUpload2( '', // archive name - 'Upload of some lame file', + 'Upload of some lame file', 'Some lame file', array( 'size' => 12345, @@ -197,7 +197,7 @@ class NewParserTest extends MediaWikiTestCase { 'mime' => 'image/jpeg', 'metadata' => serialize( array() ), 'sha1' => wfBaseConvert( '', 16, 36, 31 ), - 'fileExists' => true ), + 'fileExists' => true ), $this->db->timestamp( '20010115123500' ), $user ); } @@ -207,8 +207,8 @@ class NewParserTest extends MediaWikiTestCase { if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { $image->recordUpload2( '', // archive name - 'zomgnotcensored', - 'Borderline image', + 'zomgnotcensored', + 'Borderline image', array( 'size' => 12345, 'width' => 320, @@ -218,7 +218,7 @@ class NewParserTest extends MediaWikiTestCase { 'mime' => 'image/jpeg', 'metadata' => serialize( array() ), 'sha1' => wfBaseConvert( '', 16, 36, 31 ), - 'fileExists' => true ), + 'fileExists' => true ), $this->db->timestamp( '20010115123500' ), $user ); } @@ -326,7 +326,6 @@ class NewParserTest extends MediaWikiTestCase { 'wgExternalLinkTarget' => false, 'wgAlwaysUseTidy' => false, 'wgHtml5' => true, - 'wgCleanupPresentationalAttributes' => true, 'wgWellFormedXml' => true, 'wgAllowMicrodataAttributes' => true, 'wgAdaptiveMessageCache' => true, @@ -345,6 +344,9 @@ class NewParserTest extends MediaWikiTestCase { $this->savedGlobals = array(); + /** @since 1.20 */ + wfRunHooks( 'ParserTestGlobals', array( &$settings ) ); + foreach ( $settings as $var => $val ) { if ( array_key_exists( $var, $GLOBALS ) ) { $this->savedGlobals[$var] = $GLOBALS[$var]; @@ -380,7 +382,7 @@ class NewParserTest extends MediaWikiTestCase { # The entries saved into RepoGroup cache with previous globals will be wrong. RepoGroup::destroySingleton(); FileBackendGroup::destroySingleton(); - MessageCache::singleton()->destroyInstance(); + MessageCache::destroyInstance(); return $context; } @@ -596,7 +598,7 @@ class NewParserTest extends MediaWikiTestCase { * Run a fuzz test series * Draw input from a set of test files * - * @todo @fixme Needs some work to not eat memory until the world explodes + * @todo fixme Needs some work to not eat memory until the world explodes * * @group ParserFuzz */ diff --git a/tests/phpunit/includes/parser/ParserMethodsTest.php b/tests/phpunit/includes/parser/ParserMethodsTest.php new file mode 100644 index 00000000..dea406c3 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserMethodsTest.php @@ -0,0 +1,33 @@ +<?php + +class ParserMethodsTest extends MediaWikiLangTestCase { + + public function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ), + ); + } + + /** + * @dataProvider dataPreSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + global $wgParser; + + $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) ); + $user = new User(); + $user->setName( "127.0.0.1" ); + $popts = ParserOptions::newFromUser( $user ); + $text = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + $this->assertEquals( $expected, $text ); + } + + // TODO: Add tests for cleanSig() / cleanSigInSig(), getSection(), replaceSection(), getPreloadText() +} + diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php index 9d3499a0..fee56748 100644 --- a/tests/phpunit/includes/parser/PreprocessorTest.php +++ b/tests/phpunit/includes/parser/PreprocessorTest.php @@ -103,7 +103,7 @@ class PreprocessorTest extends MediaWikiTestCase { array( "{{foo|bar=|}", "<root>{{foo|bar=|}</root>"), array( "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>"), array( "{{Foo|} Bar=}}", "<root><template><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>"), - /* array( file_get_contents( dirname( __FILE__ ) . '/QuoteQuran.txt' ), file_get_contents( dirname( __FILE__ ) . '/QuoteQuranExpanded.txt' ) ), */ + /* array( file_get_contents( __DIR__ . '/QuoteQuran.txt' ), file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ), */ ); } @@ -165,7 +165,7 @@ class PreprocessorTest extends MediaWikiTestCase { * @dataProvider provideFiles */ function testPreprocessorOutputFiles( $filename ) { - $folder = dirname( __FILE__ ) . "/../../../parser/preprocess"; + $folder = __DIR__ . "/../../../parser/preprocess"; $wikiText = file_get_contents( "$folder/$filename.txt" ); $output = $this->preprocessToXml( $wikiText ); diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php index ea9d5533..20e42a68 100644 --- a/tests/phpunit/includes/specials/SpecialSearchTest.php +++ b/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -87,6 +87,14 @@ class SpecialSearchTest extends MediaWikiTestCase { 'advanced', array( 2, 14 ), 'Bug 33583: search with no option should honor User search preferences' ), + array( + $EMPTY_REQUEST, array_fill_keys( array_map( function( $ns ) { + return "searchNs$ns"; + }, $defaultNS ), 0 ) + array( 'searchNs2' => 1, 'searchNs14' => 1 ), + 'advanced', array( 2, 14 ), + 'Bug 33583: search with no option should honor User search preferences' + . 'and have all other namespace disabled' + ), ); } diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php index d56cce31..f66c387b 100644 --- a/tests/phpunit/includes/upload/UploadFromUrlTest.php +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -20,7 +20,7 @@ class UploadFromUrlTest extends ApiTestCase { } } - protected function doApiRequest( $params, $unused = null, $appendModule = false, $user = null ) { + protected function doApiRequest( Array $params, Array $unused = null, $appendModule = false, User $user = null ) { $sessionId = session_id(); session_write_close(); @@ -228,11 +228,11 @@ class UploadFromUrlTest extends ApiTestCase { $talk = $this->user->user->getTalkPage(); if ( $talk->exists() ) { - $a = new Article( $talk ); - $a->doDeleteArticle( '' ); + $page = WikiPage::factory( $talk ); + $page->doDeleteArticle( '' ); } - $this->assertFalse( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk does not exist' ); + $this->assertFalse( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk does not exist' ); $data = $this->doApiRequest( array( 'action' => 'upload', @@ -249,7 +249,7 @@ class UploadFromUrlTest extends ApiTestCase { $job->run(); $this->assertTrue( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ); - $this->assertTrue( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk exists' ); + $this->assertTrue( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk exists' ); $this->deleteFile( 'UploadFromUrlTest.png' ); @@ -341,8 +341,8 @@ class UploadFromUrlTest extends ApiTestCase { $file = wfFindFile( $name, array( 'ignoreRedirect' => true ) ); $empty = ""; FileDeleteForm::doDelete( $t, $file, $empty, "none", true ); - $a = new Article ( $t ); - $a->doDeleteArticle( "testing" ); + $page = WikiPage::factory( $t ); + $page->doDeleteArticle( "testing" ); } $t = Title::newFromText( $name, NS_FILE ); diff --git a/tests/phpunit/includes/upload/UploadStashTest.php b/tests/phpunit/includes/upload/UploadStashTest.php index c9dbb138..66fafaaf 100644 --- a/tests/phpunit/includes/upload/UploadStashTest.php +++ b/tests/phpunit/includes/upload/UploadStashTest.php @@ -12,17 +12,17 @@ class UploadStashTest extends MediaWikiTestCase { parent::setUp(); // Setup a file for bug 29408 - $this->bug29408File = dirname( __FILE__ ) . '/bug29408'; + $this->bug29408File = __DIR__ . '/bug29408'; file_put_contents( $this->bug29408File, "\x00" ); self::$users = array( - 'sysop' => new ApiTestUser( + 'sysop' => new TestUser( 'Uploadstashtestsysop', 'Upload Stash Test Sysop', 'upload_stash_test_sysop@example.com', array( 'sysop' ) ), - 'uploader' => new ApiTestUser( + 'uploader' => new TestUser( 'Uploadstashtestuser', 'Upload Stash Test User', 'upload_stash_test_user@example.com', diff --git a/tests/phpunit/includes/upload/UploadTest.php b/tests/phpunit/includes/upload/UploadTest.php index 4293d23b..6948f5b1 100644 --- a/tests/phpunit/includes/upload/UploadTest.php +++ b/tests/phpunit/includes/upload/UploadTest.php @@ -12,7 +12,9 @@ class UploadTest extends MediaWikiTestCase { $this->upload = new UploadTestHandler; $this->hooks = $wgHooks; - $wgHooks['InterwikiLoadPrefix'][] = 'MediaWikiTestCase::disableInterwikis'; + $wgHooks['InterwikiLoadPrefix'][] = function( $prefix, &$data ) { + return false; + }; } function tearDown() { diff --git a/tests/phpunit/languages/LanguageHeTest.php b/tests/phpunit/languages/LanguageHeTest.php index 9ac0f952..7833da71 100644 --- a/tests/phpunit/languages/LanguageHeTest.php +++ b/tests/phpunit/languages/LanguageHeTest.php @@ -18,31 +18,31 @@ class LanguageHeTest extends MediaWikiTestCase { /** @dataProvider providerPluralDual */ function testPluralDual( $result, $value ) { - $forms = array( 'one', 'many', 'two' ); + $forms = array( 'one', 'two', 'other' ); $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) ); } function providerPluralDual() { return array ( - array( 'many', 0 ), // Zero -> plural + array( 'other', 0 ), // Zero -> plural array( 'one', 1 ), // Singular array( 'two', 2 ), // Dual - array( 'many', 3 ), // Plural + array( 'other', 3 ), // Plural ); } /** @dataProvider providerPlural */ function testPlural( $result, $value ) { - $forms = array( 'one', 'many' ); + $forms = array( 'one', 'other' ); $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) ); } function providerPlural() { return array ( - array( 'many', 0 ), // Zero -> plural + array( 'other', 0 ), // Zero -> plural array( 'one', 1 ), // Singular - array( 'many', 2 ), // Plural, no dual provided - array( 'many', 3 ), // Plural + array( 'other', 2 ), // Plural, no dual provided + array( 'other', 3 ), // Plural ); } } diff --git a/tests/phpunit/languages/LanguageHuTest.php b/tests/phpunit/languages/LanguageHuTest.php new file mode 100644 index 00000000..adbd37ec --- /dev/null +++ b/tests/phpunit/languages/LanguageHuTest.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageHu.php */ +class LanguageHuTest extends MediaWikiTestCase { + private $lang; + + function setUp() { + $this->lang = Language::factory( 'Hu' ); + } + function tearDown() { + unset( $this->lang ); + } + + /** @dataProvider providePlural */ + function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array ( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageSrTest.php b/tests/phpunit/languages/LanguageSrTest.php index a50547c6..d44ecf8e 100644 --- a/tests/phpunit/languages/LanguageSrTest.php +++ b/tests/phpunit/languages/LanguageSrTest.php @@ -12,9 +12,9 @@ * @file */ -require_once dirname( dirname( __FILE__ ) ) . '/bootstrap.php'; +require_once dirname( __DIR__ ) . '/bootstrap.php'; -/** Tests for MediaWiki languages/LanguageTr.php */ +/** Tests for MediaWiki languages/LanguageSr.php */ class LanguageSrTest extends MediaWikiTestCase { /* Language object. Initialized before each test */ private $lang; @@ -65,18 +65,38 @@ class LanguageSrTest extends MediaWikiTestCase { * @author Nikola Smolenski */ function testConversionToCyrillic() { + //A simple convertion of Latin to Cyrillic $this->assertEquals( 'абвг', $this->convertToCyrillic( 'abvg' ) ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdž', + $this->convertToCyrillic( '-{lj}-ab-{nj}-vg-{dž}-' ) + ); + //A simple convertion of Cyrillic to Cyrillic $this->assertEquals( 'абвг', $this->convertToCyrillic( 'абвг' ) ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdž', + $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{dž}-' ) + ); + //This text has some Latin, but is recognized as Cyrillic, so it should not be converted $this->assertEquals( 'abvgшђжчћ', $this->convertToCyrillic( 'abvgшђжчћ' ) ); + //Same as above, but assert that -{}-s must be removed + $this->assertEquals( 'љabvgњшђжчћџ', + $this->convertToCyrillic( '-{љ}-abvg-{њ}-шђжчћ-{џ}-' ) + ); + //This text has some Cyrillic, but is recognized as Latin, so it should be converted $this->assertEquals( 'абвгшђжчћ', $this->convertToCyrillic( 'абвгšđžčć' ) ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабвгnjшђжчћdž', + $this->convertToCyrillic( '-{lj}-абвг-{nj}-šđžčć-{dž}-' ) + ); // Roman numerals are not converted $this->assertEquals( 'а I б II в III г IV шђжчћ', $this->convertToCyrillic( 'a I b II v III g IV šđžčć' ) @@ -84,15 +104,19 @@ class LanguageSrTest extends MediaWikiTestCase { } function testConversionToLatin() { + //A simple convertion of Latin to Latin $this->assertEquals( 'abcd', $this->convertToLatin( 'abcd' ) ); + //A simple convertion of Cyrillic to Latin $this->assertEquals( 'abcd', $this->convertToLatin( 'абцд' ) ); + //This text has some Latin, but is recognized as Cyrillic, so it should be converted $this->assertEquals( 'abcdšđžčć', $this->convertToLatin( 'abcdшђжчћ' ) ); + //This text has some Cyrillic, but is recognized as Latin, so it should not be converted $this->assertEquals( 'абцдšđžčć', $this->convertToLatin( 'абцдšđžčć' ) ); diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php index cb4c641d..95d8fde4 100644 --- a/tests/phpunit/languages/LanguageTest.php +++ b/tests/phpunit/languages/LanguageTest.php @@ -24,43 +24,195 @@ class LanguageTest extends MediaWikiTestCase { ); } - /** @dataProvider provideFormattableTimes */ + /** + * @dataProvider provideFormattableTimes + */ function testFormatTimePeriod( $seconds, $format, $expected, $desc ) { $this->assertEquals( $expected, $this->lang->formatTimePeriod( $seconds, $format ), $desc ); } function provideFormattableTimes() { return array( - array( 9.45, array(), '9.5 s', 'formatTimePeriod() rounding (<10s)' ), - array( 9.45, array( 'noabbrevs' => true ), '9.5 seconds', 'formatTimePeriod() rounding (<10s)' ), - array( 9.95, array(), '10 s', 'formatTimePeriod() rounding (<10s)' ), - array( 9.95, array( 'noabbrevs' => true ), '10 seconds', 'formatTimePeriod() rounding (<10s)' ), - array( 59.55, array(), '1 min 0 s', 'formatTimePeriod() rounding (<60s)' ), - array( 59.55, array( 'noabbrevs' => true ), '1 minute 0 seconds', 'formatTimePeriod() rounding (<60s)' ), - array( 119.55, array(), '2 min 0 s', 'formatTimePeriod() rounding (<1h)' ), - array( 119.55, array( 'noabbrevs' => true ), '2 minutes 0 seconds', 'formatTimePeriod() rounding (<1h)' ), - array( 3599.55, array(), '1 h 0 min 0 s', 'formatTimePeriod() rounding (<1h)' ), - array( 3599.55, array( 'noabbrevs' => true ), '1 hour 0 minutes 0 seconds', 'formatTimePeriod() rounding (<1h)' ), - array( 7199.55, array(), '2 h 0 min 0 s', 'formatTimePeriod() rounding (>=1h)' ), - array( 7199.55, array( 'noabbrevs' => true ), '2 hours 0 minutes 0 seconds', 'formatTimePeriod() rounding (>=1h)' ), - array( 7199.55, 'avoidseconds', '2 h 0 min', 'formatTimePeriod() rounding (>=1h), avoidseconds' ), - array( 7199.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 hours 0 minutes', 'formatTimePeriod() rounding (>=1h), avoidseconds' ), - array( 7199.55, 'avoidminutes', '2 h 0 min', 'formatTimePeriod() rounding (>=1h), avoidminutes' ), - array( 7199.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '2 hours 0 minutes', 'formatTimePeriod() rounding (>=1h), avoidminutes' ), - array( 172799.55, 'avoidseconds', '48 h 0 min', 'formatTimePeriod() rounding (=48h), avoidseconds' ), - array( 172799.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '48 hours 0 minutes', 'formatTimePeriod() rounding (=48h), avoidseconds' ), - array( 259199.55, 'avoidminutes', '3 d 0 h', 'formatTimePeriod() rounding (>48h), avoidminutes' ), - array( 259199.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '3 days 0 hours', 'formatTimePeriod() rounding (>48h), avoidminutes' ), - array( 176399.55, 'avoidseconds', '2 d 1 h 0 min', 'formatTimePeriod() rounding (>48h), avoidseconds' ), - array( 176399.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 days 1 hour 0 minutes', 'formatTimePeriod() rounding (>48h), avoidseconds' ), - array( 176399.55, 'avoidminutes', '2 d 1 h', 'formatTimePeriod() rounding (>48h), avoidminutes' ), - array( 176399.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '2 days 1 hour', 'formatTimePeriod() rounding (>48h), avoidminutes' ), - array( 259199.55, 'avoidseconds', '3 d 0 h 0 min', 'formatTimePeriod() rounding (>48h), avoidseconds' ), - array( 259199.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '3 days 0 hours 0 minutes', 'formatTimePeriod() rounding (>48h), avoidseconds' ), - array( 172801.55, 'avoidseconds', '2 d 0 h 0 min', 'formatTimePeriod() rounding, (>48h), avoidseconds' ), - array( 172801.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 days 0 hours 0 minutes', 'formatTimePeriod() rounding, (>48h), avoidseconds' ), - array( 176460.55, array(), '2 d 1 h 1 min 1 s', 'formatTimePeriod() rounding, recursion, (>48h)' ), - array( 176460.55, array( 'noabbrevs' => true ), '2 days 1 hour 1 minute 1 second', 'formatTimePeriod() rounding, recursion, (>48h)' ), + array( + 9.45, + array(), + '9.5 s', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.45, + array( 'noabbrevs' => true ), + '9.5 seconds', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.95, + array(), + '10 s', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.95, + array( 'noabbrevs' => true ), + '10 seconds', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 59.55, + array(), + '1 min 0 s', + 'formatTimePeriod() rounding (<60s)' + ), + array( + 59.55, + array( 'noabbrevs' => true ), + '1 minute 0 seconds', + 'formatTimePeriod() rounding (<60s)' + ), + array( + 119.55, + array(), + '2 min 0 s', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 119.55, + array( 'noabbrevs' => true ), + '2 minutes 0 seconds', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 3599.55, + array(), + '1 h 0 min 0 s', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 3599.55, + array( 'noabbrevs' => true ), + '1 hour 0 minutes 0 seconds', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 7199.55, + array(), + '2 h 0 min 0 s', + 'formatTimePeriod() rounding (>=1h)' + ), + array( + 7199.55, + array( 'noabbrevs' => true ), + '2 hours 0 minutes 0 seconds', + 'formatTimePeriod() rounding (>=1h)' + ), + array( + 7199.55, + 'avoidseconds', + '2 h 0 min', + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ), + array( + 7199.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 hours 0 minutes', + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ), + array( + 7199.55, + 'avoidminutes', + '2 h 0 min', + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ), + array( + 7199.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '2 hours 0 minutes', + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ), + array( + 172799.55, + 'avoidseconds', + '48 h 0 min', + 'formatTimePeriod() rounding (=48h), avoidseconds' + ), + array( + 172799.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '48 hours 0 minutes', + 'formatTimePeriod() rounding (=48h), avoidseconds' + ), + array( + 259199.55, + 'avoidminutes', + '3 d 0 h', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 259199.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '3 days 0 hours', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 176399.55, + 'avoidseconds', + '2 d 1 h 0 min', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 176399.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 days 1 hour 0 minutes', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 176399.55, + 'avoidminutes', + '2 d 1 h', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 176399.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '2 days 1 hour', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 259199.55, + 'avoidseconds', + '3 d 0 h 0 min', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 259199.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '3 days 0 hours 0 minutes', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 172801.55, + 'avoidseconds', + '2 d 0 h 0 min', + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ), + array( + 172801.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 days 0 hours 0 minutes', + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ), + array( + 176460.55, + array(), + '2 d 1 h 1 min 1 s', + 'formatTimePeriod() rounding, recursion, (>48h)' + ), + array( + 176460.55, + array( 'noabbrevs' => true ), + '2 days 1 hour 1 minute 1 second', + 'formatTimePeriod() rounding, recursion, (>48h)' + ), ); } @@ -98,8 +250,8 @@ class LanguageTest extends MediaWikiTestCase { } /** - * @dataProvider provideHTMLTruncateData() - */ + * @dataProvider provideHTMLTruncateData() + */ function testTruncateHtml( $len, $ellipsis, $input, $expected ) { // Actual HTML... $this->assertEquals( @@ -654,4 +806,264 @@ class LanguageTest extends MediaWikiTestCase { ), ); } + + + + /** + * @dataProvider provideFormatDuration + */ + function testFormatDuration( $duration, $expected, $intervals = array() ) { + $this->assertEquals( + $expected, + $this->lang->formatDuration( $duration, $intervals ), + "formatDuration('$duration'): $expected" + ); + } + + function provideFormatDuration() { + return array( + array( + 0, + '0 seconds', + ), + array( + 1, + '1 second', + ), + array( + 2, + '2 seconds', + ), + array( + 60, + '1 minute', + ), + array( + 2 * 60, + '2 minutes', + ), + array( + 3600, + '1 hour', + ), + array( + 2 * 3600, + '2 hours', + ), + array( + 24 * 3600, + '1 day', + ), + array( + 2 * 86400, + '2 days', + ), + array( + 365.25 * 86400, // 365.25 * 86400 = 31557600 + '1 year', + ), + array( + 2 * 31557600, + '2 years', + ), + array( + 10 * 31557600, + '1 decade', + ), + array( + 20 * 31557600, + '2 decades', + ), + array( + 100 * 31557600, + '1 century', + ), + array( + 200 * 31557600, + '2 centuries', + ), + array( + 1000 * 31557600, + '1 millennium', + ), + array( + 2000 * 31557600, + '2 millennia', + ), + array( + 9001, + '2 hours, 30 minutes and 1 second' + ), + array( + 3601, + '1 hour and 1 second' + ), + array( + 31557600 + 2 * 86400 + 9000, + '1 year, 2 days, 2 hours and 30 minutes' + ), + array( + 42 * 1000 * 31557600 + 42, + '42 millennia and 42 seconds' + ), + array( + 60, + '60 seconds', + array( 'seconds' ), + ), + array( + 61, + '61 seconds', + array( 'seconds' ), + ), + array( + 1, + '1 second', + array( 'seconds' ), + ), + array( + 31557600 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'years', 'days', 'minutes' ), + ), + array( + 42, + '0 days', + array( 'years', 'days' ), + ), + array( + 31557600 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'minutes', 'days', 'years' ), + ), + array( + 42, + '0 days', + array( 'days', 'years' ), + ), + ); + } + + /** + * @dataProvider provideCheckTitleEncodingData + */ + function testCheckTitleEncoding( $s ) { + $this->assertEquals( + $s, + $this->lang->checkTitleEncoding($s), + "checkTitleEncoding('$s')" + ); + } + + function provideCheckTitleEncodingData() { + return array ( + array( "" ), + array( "United States of America" ), // 7bit ASCII + array( rawurldecode( "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e" ) ), + array( + rawurldecode( + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn" + ) + ), + // The following two data sets come from bug 36839. They fail if checkTitleEncoding uses a regexp to test for + // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding + // uses mb_check_encoding for its test. + array( + rawurldecode( + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C" + . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C" + . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C" + . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C" + . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C" + . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C" + . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C" + . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C" + . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C" + . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C" + . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C" + . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C" + . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C" + . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis" + ), + ), + array( + rawurldecode( + "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C" + . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C" + . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C" + . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C" + . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou" + . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C" + . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C" + . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C" + . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C" + . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C" + . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C" + . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes" + ) + ) + ); + } + + /** + * @dataProvider provideRomanNumeralsData + */ + function testRomanNumerals( $num, $numerals ) { + $this->assertEquals( + $numerals, + Language::romanNumeral( $num ), + "romanNumeral('$num')" + ); + } + + function provideRomanNumeralsData() { + return array( + array( 1, 'I' ), + array( 2, 'II' ), + array( 3, 'III' ), + array( 4, 'IV' ), + array( 5, 'V' ), + array( 6, 'VI' ), + array( 7, 'VII' ), + array( 8, 'VIII' ), + array( 9, 'IX' ), + array( 10, 'X' ), + array( 20, 'XX' ), + array( 30, 'XXX' ), + array( 40, 'XL' ), + array( 49, 'XLIX' ), + array( 50, 'L' ), + array( 60, 'LX' ), + array( 70, 'LXX' ), + array( 80, 'LXXX' ), + array( 90, 'XC' ), + array( 99, 'XCIX' ), + array( 100, 'C' ), + array( 200, 'CC' ), + array( 300, 'CCC' ), + array( 400, 'CD' ), + array( 500, 'D' ), + array( 600, 'DC' ), + array( 700, 'DCC' ), + array( 800, 'DCCC' ), + array( 900, 'CM' ), + array( 999, 'CMXCIX' ), + array( 1000, 'M' ), + array( 1989, 'MCMLXXXIX' ), + array( 2000, 'MM' ), + array( 3000, 'MMM' ), + array( 4000, 'MMMM' ), + array( 5000, 'MMMMM' ), + array( 6000, 'MMMMMM' ), + array( 7000, 'MMMMMMM' ), + array( 8000, 'MMMMMMMM' ), + array( 9000, 'MMMMMMMMM' ), + array( 9999, 'MMMMMMMMMCMXCIX'), + array( 10000, 'MMMMMMMMMM' ), + ); + } } + diff --git a/tests/phpunit/languages/LanguageUzTest.php b/tests/phpunit/languages/LanguageUzTest.php new file mode 100644 index 00000000..72387283 --- /dev/null +++ b/tests/phpunit/languages/LanguageUzTest.php @@ -0,0 +1,120 @@ +<?php +/** + * PHPUnit tests for the Uzbek language. + * The language can be represented using two scripts: + * - Latin (uz-latn) + * - Cyrillic (uz-cyrl) + * + * @author Robin Pepermans + * @author Antoine Musso <hashar at free dot fr> + * @copyright Copyright © 2012, Robin Pepermans + * @copyright Copyright © 2011, Antoine Musso <hashar at free dot fr> + * @file + */ + +require_once dirname( __DIR__ ) . '/bootstrap.php'; + +/** Tests for MediaWiki languages/LanguageUz.php */ +class LanguageUzTest extends MediaWikiTestCase { + /* Language object. Initialized before each test */ + private $lang; + + function setUp() { + $this->lang = Language::factory( 'uz' ); + } + function tearDown() { + unset( $this->lang ); + } + + /** + * @author Nikola Smolenski + */ + function testConversionToCyrillic() { + // A convertion of Latin to Cyrillic + $this->assertEquals( 'абвгғ', + $this->convertToCyrillic( 'abvggʻ' ) + ); + // Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгўоdb', + $this->convertToCyrillic( '-{lj}-ab-{nj}-vgoʻo-{db}-' ) + ); + // A simple convertion of Cyrillic to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'абвг' ) + ); + // Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdaž', + $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{da}-ž' ) + ); + } + + function testConversionToLatin() { + // A simple convertion of Latin to Latin + $this->assertEquals( 'abdef', + $this->convertToLatin( 'abdef' ) + ); + // A convertion of Cyrillic to Latin + $this->assertEquals( 'gʻabtsdOʻQyo', + $this->convertToLatin( 'ғабцдЎҚё' ) + ); + } + + ##### HELPERS ##################################################### + /** + * Wrapper to verify text stay the same after applying conversion + * @param $text string Text to convert + * @param $variant string Language variant 'uz-cyrl' or 'uz-latn' + * @param $msg string Optional message + */ + function assertUnConverted( $text, $variant, $msg = '' ) { + $this->assertEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + /** + * Wrapper to verify a text is different once converted to a variant. + * @param $text string Text to convert + * @param $variant string Language variant 'uz-cyrl' or 'uz-latn' + * @param $msg string Optional message + */ + function assertConverted( $text, $variant, $msg = '' ) { + $this->assertNotEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Verifiy the given Cyrillic text is not converted when using + * using the cyrillic variant and converted to Latin when using + * the Latin variant. + */ + function assertCyrillic( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'uz-cyrl', $msg ); + $this->assertConverted( $text, 'uz-latn', $msg ); + } + /** + * Verifiy the given Latin text is not converted when using + * using the Latin variant and converted to Cyrillic when using + * the Cyrillic variant. + */ + function assertLatin( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'uz-latn', $msg ); + $this->assertConverted( $text, 'uz-cyrl', $msg ); + } + + + /** Wrapper for converter::convertTo() method*/ + function convertTo( $text, $variant ) { + return $this->lang->mConverter->convertTo( $text, $variant ); + } + function convertToCyrillic( $text ) { + return $this->convertTo( $text, 'uz-cyrl' ); + } + function convertToLatin( $text ) { + return $this->convertTo( $text, 'uz-latn' ); + } +} diff --git a/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php new file mode 100644 index 00000000..033164b0 --- /dev/null +++ b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php @@ -0,0 +1,95 @@ +<?php +/** + * @author Niklas Laxström + * @file + */ + +class CLDRPluralRuleEvaluatorTest extends MediaWikiTestCase { + /** + * @dataProvider validTestCases + */ + function testValidRules( $expected, $rules, $number, $comment ) { + $result = CLDRPluralRuleEvaluator::evaluate( $number, (array) $rules ); + $this->assertEquals( $expected, $result, $comment ); + } + + /** + * @dataProvider invalidTestCases + * @expectedException CLDRPluralRuleError + */ + function testInvalidRules( $rules, $comment ) { + CLDRPluralRuleEvaluator::evaluate( 1, (array) $rules ); + } + + function validTestCases() { + $tests = array( + # expected, number, rule, comment + array( 0, 'n is 1', 1, 'integer number and is' ), + array( 0, 'n is 1', "1", 'string integer number and is' ), + array( 0, 'n is 1', 1.0, 'float number and is' ), + array( 0, 'n is 1', "1.0", 'string float number and is' ), + array( 1, 'n is 1', 1.1, 'float number and is' ), + array( 1, 'n is 1', 2, 'float number and is' ), + + array( 0, 'n in 1,3,5', 3, '' ), + array( 1, 'n not in 1,3,5', 5, '' ), + + array( 1, 'n in 1,3,5', 2, '' ), + array( 0, 'n not in 1,3,5', 4, '' ), + + array( 0, 'n in 1..3', 2, '' ), + array( 0, 'n in 1..3', 3, 'in is inclusive' ), + array( 1, 'n in 1..3', 0, '' ), + + array( 1, 'n not in 1..3', 2, '' ), + array( 1, 'n not in 1..3', 3, 'in is inclusive' ), + array( 0, 'n not in 1..3', 0, '' ), + + array( 1, 'n is not 1 and n is not 2 and n is not 3', 1, 'and relation' ), + array( 0, 'n is not 1 and n is not 2 and n is not 4', 3, 'and relation' ), + + array( 0, 'n is not 1 or n is 1', 1, 'or relation' ), + array( 1, 'n is 1 or n is 2', 3, 'or relation' ), + + array( 0, 'n is 1', 1, 'extra whitespace' ), + + array( 0, 'n mod 3 is 1', 7, 'mod' ), + array( 0, 'n mod 3 is not 1', 4.3, 'mod with floats' ), + + array( 0, 'n within 1..3', 2, 'within with integer' ), + array( 0, 'n within 1..3', 2.5, 'within with float' ), + array( 0, 'n in 1..3', 2, 'in with integer' ), + array( 1, 'n in 1..3', 2.5, 'in with float' ), + + array( 0, 'n in 3 or n is 4 and n is 5', 3, 'and binds more tightly than or' ), + array( 1, 'n is 3 or n is 4 and n is 5', 4, 'and binds more tightly than or' ), + + array( 0, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 24, 'breton rule' ), + array( 1, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 25, 'breton rule' ), + + array( 0, 'n within 0..2 and n is not 2', 0, 'french rule' ), + array( 0, 'n within 0..2 and n is not 2', 1, 'french rule' ), + array( 0, 'n within 0..2 and n is not 2', 1.2, 'french rule' ), + array( 1, 'n within 0..2 and n is not 2', 2, 'french rule' ), + + array( 1, 'n in 3..10,13..19', 2, 'scottish rule - ranges with comma' ), + array( 0, 'n in 3..10,13..19', 4, 'scottish rule - ranges with comma' ), + array( 1, 'n in 3..10,13..19', 12.999, 'scottish rule - ranges with comma' ), + array( 0, 'n in 3..10,13..19', 13, 'scottish rule - ranges with comma' ), + + array( 0, '5 mod 3 is n', 2, 'n as result of mod - no need to pass' ), + ); + + return $tests; + } + + function invalidTestCases() { + $tests = array( + array( 'n mod mod 5 is 1', 'mod mod' ), + array( 'n', 'just n' ), + array( 'n is in 5', 'is in' ), + ); + return $tests; + } + +} diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php new file mode 100644 index 00000000..d1344389 --- /dev/null +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -0,0 +1,352 @@ +<?php + +/** + * Base TestCase for dumps + */ +abstract class DumpTestCase extends MediaWikiLangTestCase { + + /** + * exception to be rethrown once in sound PHPUnit surrounding + * + * As the current MediaWikiTestCase::run is not robust enough to recover + * from thrown exceptions directly, we cannot throw frow within + * self::addDBData, although it would be appropriate. Hence, we catch the + * exception and store it until we are in setUp and may finally rethrow + * the exception without crashing the test suite. + * + * @var Exception|null + */ + protected $exceptionFromAddDBData = null; + + /** + * Holds the xmlreader used for analyzing an xml dump + * + * @var XMLReader|null + */ + protected $xml = null; + + /** + * Adds a revision to a page, while returning the resuting revision's id + * + * @param $page WikiPage: page to add the revision to + * @param $text string: revisions text + * @param $text string: revisions summare + * + * @throws MWExcepion + */ + protected function addRevision( Page $page, $text, $summary ) { + $status = $page->doEdit( $text, $summary ); + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $revision_id = $revision->getId(); + $text_id = $revision->getTextId(); + if ( ( $revision_id > 0 ) && ( $text_id > 0 ) ) { + return array( $revision_id, $text_id ); + } + } + throw new MWException( "Could not determine revision id (" . $status->getWikiText() . ")" ); + } + + + /** + * gunzips the given file and stores the result in the original file name + * + * @param $fname string: filename to read the gzipped data from and stored + * the gunzipped data into + */ + protected function gunzip( $fname ) { + $gzipped_contents = file_get_contents( $fname ); + if ( $gzipped_contents === FALSE ) { + $this->fail( "Could not get contents of $fname" ); + } + // We resort to use gzinflate instead of gzdecode, as gzdecode + // need not be available + $contents = gzinflate( substr( $gzipped_contents, 10, -8 ) ); + $this->assertEquals( strlen( $contents ), + file_put_contents( $fname, $contents ), "# bytes written" ); + } + + /** + * Default set up function. + * + * Clears $wgUser, and reports errors from addDBData to PHPUnit + */ + public function setUp() { + global $wgUser; + + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + // @see self::exceptionFromAddDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $wgUser = new User(); + } + + /** + * Checks for test output consisting only of lines containing ETA announcements + */ + function expectETAOutput() { + // Newer PHPUnits require assertion about the output using PHPUnit's own + // expectOutput[...] functions. However, the PHPUnit shipped prediactes + // do not allow to check /each/ line of the output using /readable/ REs. + // So we ... + // + // 1. ... add a dummy output checking to make PHPUnit not complain + // about unchecked test output + $this->expectOutputRegex( '//' ); + + // 2. Do the real output checking on our own. + $lines = explode( "\n", $this->getActualOutput() ); + $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" ); + $this->assertEquals( '', array_pop( $lines ), "Output ends in LF" ); + $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]"; + foreach ( $lines as $line ) { + $this->assertRegExp( "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/", $line ); + } + } + + + /** + * Step the current XML reader until node end of given name is found. + * + * @param $name string: name of the closing element to look for + * (e.g.: "mediawiki" when looking for </mediawiki>) + * + * @return bool: true iff the end node could be found. false otherwise. + */ + protected function skipToNodeEnd( $name ) { + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::END_ELEMENT && + $this->xml->name == $name ) { + return true; + } + } + return false; + } + + /** + * Step the current XML reader to the first element start after the node + * end of a given name. + * + * @param $name string: name of the closing element to look for + * (e.g.: "mediawiki" when looking for </mediawiki>) + * + * @return bool: true iff new element after the closing of $name could be + * found. false otherwise. + */ + protected function skipPastNodeEnd( $name ) { + $this->assertTrue( $this->skipToNodeEnd( $name ), + "Skipping to end of $name" ); + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::ELEMENT ) { + return true; + } + } + return false; + } + + /** + * Opens an XML file to analyze and optionally skips past siteinfo. + * + * @param $fname string: name of file to analyze + * @param $skip_siteinfo bool: (optional) If true, step the xml reader + * to the first element after </siteinfo> + */ + protected function assertDumpStart( $fname, $skip_siteinfo = true ) { + $this->xml = new XMLReader(); + $this->assertTrue( $this->xml->open( $fname ), + "Opening temporary file $fname via XMLReader failed" ); + if ( $skip_siteinfo ) { + $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ), + "Skipping past end of siteinfo" ); + } + } + + /** + * Asserts that the xml reader is at the final closing tag of an xml file and + * closes the reader. + * + * @param $tag string: (optional) the name of the final tag + * (e.g.: "mediawiki" for </mediawiki>) + */ + protected function assertDumpEnd( $name = "mediawiki" ) { + $this->assertNodeEnd( $name, false ); + if ( $this->xml->read() ) { + $this->skipWhitespace(); + } + $this->assertEquals( $this->xml->nodeType, XMLReader::NONE, + "No proper entity left to parse" ); + $this->xml->close(); + } + + /** + * Steps the xml reader over white space + */ + protected function skipWhitespace() { + $cont = true; + while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE ) + || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { + $cont = $this->xml->read(); + } + } + + /** + * Asserts that the xml reader is at an element of given name, and optionally + * skips past it. + * + * @param $name string: the name of the element to check for + * (e.g.: "mediawiki" for <mediawiki>) + * @param $skip bool: (optional) if true, skip past the found element + */ + protected function assertNodeStart( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past start tag" ); + } + } + + /** + * Asserts that the xml reader is at an closing element of given name, and optionally + * skips past it. + * + * @param $name string: the name of the closing element to check for + * (e.g.: "mediawiki" for </mediawiki>) + * @param $skip bool: (optional) if true, skip past the found element + */ + protected function assertNodeEnd( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past end tag" ); + } + } + + + /** + * Asserts that the xml reader is at an element of given tag that contains a given text, + * and skips over the element. + * + * @param $name string: the name of the element to check for + * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>) + * @param $text string|false: If string, check if it equals the elements text. + * If false, ignore the element's text + * @param $skip_ws bool: (optional) if true, skip past white spaces that trail the + * closing element. + */ + protected function assertTextNode( $name, $text, $skip_ws = true ) { + $this->assertNodeStart( $name ); + + if ( $text !== false ) { + $this->assertEquals( $text, $this->xml->value, "Text of node " . $name ); + } + $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); + $this->assertNodeEnd( $name ); + + if ( $skip_ws ) { + $this->skipWhitespace(); + } + } + + /** + * Asserts that the xml reader is at the start of a page element and skips over the first + * tags, after checking them. + * + * Besides the opening page element, this function also checks for and skips over the + * title, ns, and id tags. Hence after this function, the xml reader is at the first + * revision of the current page. + * + * @param $id int: id of the page to assert + * @param $ns int: number of namespage to assert + * @param $name string: title of the current page + */ + protected function assertPageStart( $id, $ns, $name ) { + + $this->assertNodeStart( "page" ); + $this->skipWhitespace(); + + $this->assertTextNode( "title", $name ); + $this->assertTextNode( "ns", $ns ); + $this->assertTextNode( "id", $id ); + + } + + /** + * Asserts that the xml reader is at the page's closing element and skips to the next + * element. + */ + protected function assertPageEnd() { + $this->assertNodeEnd( "page" ); + $this->skipWhitespace(); + } + + /** + * Asserts that the xml reader is at a revision and checks its representation before + * skipping over it. + * + * @param $id int: id of the revision + * @param $summary string: summary of the revision + * @param $text_id int: id of the revision's text + * @param $text_bytes int: # of bytes in the revision's text + * @param $text_sha1 string: the base36 SHA-1 of the revision's text + * @param $text string|false: (optional) The revision's string, or false to check for a + * revision stub + * @param $parentid int|false: (optional) id of the parent revision + */ + protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) { + + $this->assertNodeStart( "revision" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + if ( $parentid !== false ) { + $this->assertTextNode( "parentid", $parentid ); + } + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "ip", false ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + $this->assertTextNode( "comment", $summary ); + + $this->assertTextNode( "sha1", $text_sha1 ); + + $this->assertNodeStart( "text", false ); + if ( $text_bytes !== false ) { + $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, + "Attribute 'bytes' of revision " . $id ); + } + + if ( $text === false ) { + // Testing for a stub + $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id, + "Text id of revision " . $id ); + $this->assertFalse( $this->xml->hasValue, "Revision has text" ); + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) + && ( $this->xml->name == "text" ) ) { + + $this->xml->read(); + } + $this->skipWhitespace(); + } else { + // Testing for a real dump + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id ); + $this->assertTrue( $this->xml->read(), "Skipping past text" ); + $this->assertNodeEnd( "text" ); + $this->skipWhitespace(); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + +} diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php new file mode 100644 index 00000000..4a6f08fa --- /dev/null +++ b/tests/phpunit/maintenance/MaintenanceTest.php @@ -0,0 +1,812 @@ +<?php + +// It would be great if we were able to use PHPUnit's getMockForAbstractClass +// instead of the MaintenanceFixup hack below. However, we cannot do +// without changing the visibility and without working around hacks in +// Maintenance.php +// +// For the same reason, we cannot just use FakeMaintenance. + +/** + * makes parts of the API of Maintenance that is hidden by protected visibily + * visible for testing, and makes up for a stream closing hack in Maintenance.php. + * + * This class is solely used for being able to test Maintenance right now + * without having to apply major refactorings to fix some design issues in + * Maintenance.php. Before adding more functions here, please consider whether + * this approach is correct, or a refactoring Maintenance to separate concers + * is more appropriate. + * + * Upon refactoring, keep in mind that besides the maintenance scrits themselves + * and tests right here, also at least Extension:Maintenance make use of + * Maintenance. + * + * Due to a hack in Maintenance.php using register_shutdown_function, be sure to + * finally call simulateShutdown on MaintenanceFixup instance before a test + * ends. + * + */ +class MaintenanceFixup extends Maintenance { + + // --- Making up for the register_shutdown_function hack in Maintenance.php + + /** + * The test case that generated this instance. + * + * This member is motivated by allowing the destructor to check whether or not + * the test failed, in order to avoid unnecessary nags about omitted shutdown + * simulation. + * But as it is already available, we also usi it to flagging tests as failed + * + * @var MediaWikiTestCase + */ + private $testCase; + + /** + * shutdownSimulated === true iff simulateShutdown has done it's work + * + * @var bool + */ + private $shutdownSimulated = false; + + /** + * Simulates what Maintenance wants to happen at script's end. + */ + public function simulateShutdown() { + + if ( $this->shutdownSimulated ) { + $this->testCase->fail( __METHOD__ . " called more than once" ); + } + + // The cleanup action. + $this->outputChanneled( false ); + + // Bookkeeping that we simulated the clean up. + $this->shutdownSimulated = true; + } + + // Note that the "public" here does not change visibility + public function outputChanneled( $msg, $channel = null ) { + if ( $this->shutdownSimulated ) { + if ( $msg !== false ) { + $this->testCase->fail( "Already past simulated shutdown, but msg is " + . "not false. Did the hack in Maintenance.php change? Please " + . "adapt the test case or Maintenance.php" ); + } + + // The current call is the one registered via register_shutdown_function. + // We can safely ignore it, as we simulated this one via simulateShutdown + // before (if we did not, the destructor of this instance will warn about + // it) + return; + } + + return call_user_func_array ( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + /** + * Safety net around register_shutdown_function of Maintenance.php + */ + public function __destruct() { + if ( ( ! $this->shutdownSimulated ) && ( ! $this->testCase->hasFailed() ) ) { + // Someone generated a MaintenanceFixup instance without calling + // simulateShutdown. We'd have to raise a PHPUnit exception to correctly + // flag this illegal usage. However, we are already in a destruktor, which + // would trigger undefined behaviour. Hence, we can only report to the + // error output :( Hopefully people read the PHPUnit output. + fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " destructed without " + . "calling simulateShutdown method. Call simulateShutdown on the " + . "instance before it gets destructed." ); + } + + // The following guard is required, as PHP does not offer default destructors :( + if ( is_callable( "parent::__destruct" ) ) { + parent::__destruct(); + } + } + + public function __construct( MediaWikiTestCase $testCase ) { + parent::__construct(); + $this->testCase = $testCase; + } + + + + // --- Making protected functions visible for test + + public function output( $out, $channel = null ) { + // Just to make PHP not nag about signature mismatches, we copied + // Maintenance::output signature. However, we do not use (or rely on) + // those variables. Instead we pass to Maintenance::output whatever we + // receive at runtime. + return call_user_func_array ( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + + + // --- Requirements for getting instance of abstract class + + public function execute() { + $this->testCase->fail( __METHOD__ . " called unexpectedly" ); + } + +} + +class MaintenanceTest extends MediaWikiTestCase { + + + /** + * The main Maintenance instance that is used for testing. + * + * @var MaintenanceFixup + */ + private $m; + + + protected function setUp() { + parent::setUp(); + $this->m = new MaintenanceFixup( $this ); + } + + + /** + * asserts the output before and after simulating shutdown + * + * This function simulates shutdown of self::m. + * + * @param $preShutdownOutput string: expected output before simulating shutdown + * @param $expectNLAppending bool: Whether or not shutdown simulation is expected + * to add a newline to the output. If false, $preShutdownOutput is the + * expected output after shutdown simulation. Otherwise, + * $preShutdownOutput with an appended newline is the expected output + * after shutdown simulation. + */ + private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { + + $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), + "Output before shutdown simulation" ); + + $this->m->simulateShutdown(); + + $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); + $this->expectOutputString( $postShutdownOutput ); + } + + + // Although the following tests do not seem to be too consistent (compare for + // example the newlines within the test.*StringString tests, or the + // test.*Intermittent.* tests), the objective of these tests is not to describe + // consistent behaviour, but rather currently existing behaviour. + + + function testOutputEmpty() { + $this->m->output( "" ); + $this->assertOutputPrePostShutdown( "", False ); + } + + function testOutputString() { + $this->m->output( "foo" ); + $this->assertOutputPrePostShutdown( "foo", False ); + } + + function testOutputStringString() { + $this->m->output( "foo" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testOutputStringNL() { + $this->m->output( "foo\n" ); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testOutputStringNLNL() { + $this->m->output( "foo\n\n" ); + $this->assertOutputPrePostShutdown( "foo\n\n", False ); + } + + function testOutputStringNLString() { + $this->m->output( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar", False ); + } + + function testOutputStringNLStringNL() { + $this->m->output( "foo\nbar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputStringNLStringNLLinewise() { + $this->m->output( "foo\n" ); + $this->m->output( "bar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputStringNLStringNLArbitrary() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\n" ); + $this->m->output( "ba" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputStringNLStringNLArbitraryAgain() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\nb" ); + $this->m->output( "a" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputWNullChannelEmpty() { + $this->m->output( "", null ); + $this->assertOutputPrePostShutdown( "", False ); + } + + function testOutputWNullChannelString() { + $this->m->output( "foo", null ); + $this->assertOutputPrePostShutdown( "foo", False ); + } + + function testOutputWNullChannelStringString() { + $this->m->output( "foo", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testOutputWNullChannelStringNL() { + $this->m->output( "foo\n", null ); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testOutputWNullChannelStringNLNL() { + $this->m->output( "foo\n\n", null ); + $this->assertOutputPrePostShutdown( "foo\n\n", False ); + } + + function testOutputWNullChannelStringNLString() { + $this->m->output( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar", False ); + } + + function testOutputWNullChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputWNullChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", null ); + $this->m->output( "bar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputWNullChannelStringNLStringNLArbitrary() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\n", null ); + $this->m->output( "ba", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\nb", null ); + $this->m->output( "a", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputWChannelString() { + $this->m->output( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", True ); + } + + function testOutputWChannelStringNL() { + $this->m->output( "foo\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", True ); + } + + function testOutputWChannelStringNLNL() { + // If this test fails, note that output takes strings with double line + // endings (although output's implementation in this situation calls + // outputChanneled with a string ending in a nl ... which is not allowed + // according to the documentation of outputChanneled) + $this->m->output( "foo\n\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\n", True ); + } + + function testOutputWChannelStringNLString() { + $this->m->output( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputWChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputWChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", True ); + } + + function testOutputWChannelStringNLStringNLArbitrary() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\n", "bazChannel" ); + $this->m->output( "ba", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", True ); + } + + function testOutputWChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\nb", "bazChannel" ); + $this->m->output( "a", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputWMultipleChannelsChannelChange() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", True ); + } + + function testOutputWMultipleChannelsChannelChangeNL() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->m->output( "qux\n", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", True ); + } + + function testOutputWAndWOChannelStringStartWO() { + $this->m->output( "foo" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux" ); + $this->m->output( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nquxquux", True ); + } + + function testOutputWAndWOChannelStringStartW() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar" ); + $this->m->output( "qux", "bazChannel" ); + $this->m->output( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbarqux\nquux", False ); + } + + function testOutputWChannelTypeSwitch() { + $this->m->output( "foo", 1 ); + $this->m->output( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputIntermittentEmpty() { + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testOutputIntermittentFalse() { + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testOutputIntermittentFalseAfterOtherChannel() { + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "qux\nfoobar", False ); + } + + function testOutputWNullChannelIntermittentEmpty() { + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testOutputWNullChannelIntermittentFalse() { + $this->m->output( "foo", null ); + $this->m->output( false, null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testOutputWChannelIntermittentEmpty() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", True ); + } + + function testOutputWChannelIntermittentFalse() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( false, "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", True ); + } + + // Note that (per documentation) outputChanneled does take strings that end + // in \n, hence we do not test such strings. + + function testOutputChanneledEmpty() { + $this->m->outputChanneled( "" ); + $this->assertOutputPrePostShutdown( "\n", False ); + } + + function testOutputChanneledString() { + $this->m->outputChanneled( "foo" ); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testOutputChanneledStringString() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputChanneledStringNLString() { + $this->m->outputChanneled( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputChanneledStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "\nb" ); + $this->m->outputChanneled( "a" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "r" ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", False ); + } + + function testOutputChanneledWNullChannelEmpty() { + $this->m->outputChanneled( "", null ); + $this->assertOutputPrePostShutdown( "\n", False ); + } + + function testOutputChanneledWNullChannelString() { + $this->m->outputChanneled( "foo", null ); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testOutputChanneledWNullChannelStringString() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputChanneledWNullChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputChanneledWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "\nb", null ); + $this->m->outputChanneled( "a", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "r", null ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", False ); + } + + function testOutputChanneledWChannelString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", True ); + } + + function testOutputChanneledWChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputChanneledWChannelStringString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", True ); + } + + function testOutputChanneledWChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "\nb", "bazChannel" ); + $this->m->outputChanneled( "a", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "r", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputChanneledWMultipleChannelsChannelChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux", "quuxChannel" ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", True ); + } + + function testOutputChanneledWMultipleChannelsChannelChangeEnclosedNull() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", True ); + } + + function testOutputChanneledWMultipleChannelsChannelAfterNullChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "quuxChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", True ); + } + + function testOutputChanneledWAndWOChannelStringStartWO() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux" ); + $this->m->outputChanneled( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux", True ); + } + + function testOutputChanneledWAndWOChannelStringStartW() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar" ); + $this->m->outputChanneled( "qux", "bazChannel" ); + $this->m->outputChanneled( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux\n", False ); + } + + function testOutputChanneledWChannelTypeSwitch() { + $this->m->outputChanneled( "foo", 1 ); + $this->m->outputChanneled( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testOutputChanneledWOChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", False ); + } + + function testOutputChanneledWOChannelIntermittentFalse() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( false ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputChanneledWNullChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", False ); + } + + function testOutputChanneledWNullChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( false, null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testOutputChanneledWChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", True ); + } + + function testOutputChanneledWChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( false, "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", True ); + } + + function testCleanupChanneledClean() { + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "", False ); + } + + function testCleanupChanneledAfterOutput() { + $this->m->output( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", False ); + } + + function testCleanupChanneledAfterOutputWNullChannel() { + $this->m->output( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", False ); + } + + function testCleanupChanneledAfterOutputWChannel() { + $this->m->output( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testCleanupChanneledAfterNLOutput() { + $this->m->output( "foo\n" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testCleanupChanneledAfterNLOutputWNullChannel() { + $this->m->output( "foo\n", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testCleanupChanneledAfterNLOutputWChannel() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testCleanupChanneledAfterOutputChanneledWOChannel() { + $this->m->outputChanneled( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testCleanupChanneledAfterOutputChanneledWNullChannel() { + $this->m->outputChanneled( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testCleanupChanneledAfterOutputChanneledWChannel() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", False ); + } + + function testMultipleMaintenanceObjectsInteractionOutput() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo" ); + $m2->output( "bar" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", null ); + $m2->output( "bar", null ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", False ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", "bazChannel" ); + $m2->output( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", True ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", null ); + $m2->output( "bar\n", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", "bazChannel" ); + $m2->output( "bar\n", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", True ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneled() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo" ); + $m2->outputChanneled( "bar" ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", null ); + $m2->outputChanneled( "bar", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", False ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", True ); + } + + function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before first cleanup" ); + $this->m->cleanupChanneled(); + $this->assertEquals( "foobar\n", $this->getActualOutput(), + "Output after first cleanup" ); + $m2->cleanupChanneled(); + $this->assertEquals( "foobar\n\n", $this->getActualOutput(), + "Output after second cleanup" ); + + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n\n", False ); + } + + +}
\ No newline at end of file diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php new file mode 100644 index 00000000..8ff85574 --- /dev/null +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -0,0 +1,270 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc"; + +/** + * Tests for BaseDump + * + * @group Dump + */ +class BaseDumpTest extends MediaWikiTestCase { + + /** + * @var BaseDump the BaseDump instance used within a test. + * + * If set, this BaseDump gets automatically closed in tearDown. + */ + private $dump = null; + + protected function tearDown() { + if ( $this->dump !== null ) { + $this->dump->close(); + } + + // Bug 37458, parent teardown need to be done after closing the + // dump or it might cause some permissions errors. + parent::tearDown(); + } + + /** + * asserts that a prefetch yields an expected string + * + * @param $expected string|null: the exepcted result of the prefetch + * @param $page int: the page number to prefetch the text for + * @param $revision int: the revision number to prefetch the text for + */ + private function assertPrefetchEquals( $expected, $page, $revision ) { + $this->assertEquals( $expected, $this->dump->prefetch( $page, $revision ), + "Prefetch of page $page revision $revision" ); + + } + + function testSequential() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeRevisionMissToRevision() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 3 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + } + + function testSynchronizeRevisionMissToPage() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizePageMiss() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 3, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testPageMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 6, 40 ); + } + + function testRevisionMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 4, 40 ); + } + + function testSynchronizePageMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 0, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSynchronizeRevisionMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 1, -2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSequentialAcrossFiles() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2, 4 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeSkipAcrossFile() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $fname3 = $this->setUpPrefetch( array( 4 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 . ";" . $fname3 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeMissInWholeFirstFile() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + + /** + * Constructs a temporary file that can be used for prefetching + * + * The temporary file is removed by DumpBackup upon tearDown. + * + * @param $requested_pages Array The indices of the page parts that should + * go into the prefetch file. 1,2,4 are available. + * @return String The file name of the created temporary file + */ + private function setUpPrefetch( $requested_pages = array( 1, 2, 4 ) ) { + // The file name, where we store the prepared prefetch file + $fname = $this->getNewTempFile(); + + // The header of every prefetch file + $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.20alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + + + // An array holding the pages that are available for prefetch + $available_pages = array(); + + // Simple plain page + $available_pages[1] = ' <page> + <title>BackupDumperTestP1</title> + <ns>0</ns> + <id>1</id> + <revision> + <id>1</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP1Summary1</comment> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + <text xml:space="preserve">BackupDumperTestP1Text1</text> + </revision> + </page> +'; + // Page with more than one revisions. Hole in rev ids. + $available_pages[2] = ' <page> + <title>BackupDumperTestP2</title> + <ns>0</ns> + <id>2</id> + <revision> + <id>2</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary1</comment> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + <text xml:space="preserve">BackupDumperTestP2Text1</text> + </revision> + <revision> + <id>5</id> + <parentid>2</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary4 extra</comment> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text> + </revision> + </page> +'; + // Page with id higher than previous id + 1 + $available_pages[4] = ' <page> + <title>Talk:BackupDumperTestP1</title> + <ns>1</ns> + <id>4</id> + <revision> + <id>8</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>Talk BackupDumperTestP1 Summary1</comment> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text> + </revision> + </page> +'; + + // The common ending for all files + $tail = '</mediawiki> +'; + + // Putting together the content of the prefetch files + $content = $header; + foreach ( $requested_pages as $i ) { + $this->assertTrue( array_key_exists( $i, $available_pages ), + "Check for availability of requested page " . $i ); + $content .= $available_pages[ $i ]; + } + $content .= $tail; + + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared prefetch" ); + + return $fname; + } + +} diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php new file mode 100644 index 00000000..a0bbadf9 --- /dev/null +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -0,0 +1,563 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupTextPass.inc"; + +/** + * Tests for page dumps of BackupDumper + * + * @group Database + * @group Dump + */ +class TextPassDumperTest extends DumpTestCase { + + // We'll add several pages, revision and texts. The following variables hold the + // corresponding ids. + private $pageId1, $pageId2, $pageId3, $pageId4; + private static $numOfPages = 4; + private $revId1_1, $textId1_1; + private $revId2_1, $textId2_1, $revId2_2, $textId2_2; + private $revId2_3, $textId2_3, $revId2_4, $textId2_4; + private $revId3_1, $textId3_1, $revId3_2, $textId3_2; + private $revId4_1, $textId4_1; + private static $numOfRevs = 8; + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + // Simple page + $title = Title::newFromText( 'BackupDumperTestP1' ); + $page = WikiPage::factory( $title ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + // Page with more than one revision + $title = Title::newFromText( 'BackupDumperTestP2' ); + $page = WikiPage::factory( $title ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + // Deleted page. + $title = Title::newFromText( 'BackupDumperTestP3' ); + $page = WikiPage::factory( $title ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + // Page from non-default namespace + $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK ); + $page = WikiPage::factory( $title ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + + } + + public function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + + } + + function testPlain() { + // Setting up the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array ( "--stub=file:" . $nameStub, + "--output=file:" . $nameFull ) ); + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testPrefetchPlain() { + // The mapping between ids and text, for the hits of the prefetch mock + $prefetchMap = array( + array( $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ), + array( $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" ) + ); + + // The mock itself + $prefetchMock = $this->getMock( 'BaseDump', array( 'prefetch' ), array(), '', FALSE ); + $prefetchMock->expects( $this->exactly( 6 ) ) + ->method( 'prefetch' ) + ->will( $this->returnValueMap( $prefetchMap ) ); + + // Setting up of the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array ( "--stub=file:" + . $nameStub, "--output=file:" . $nameFull ) ); + $dumper->prefetch = $prefetchMock; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "Prefetch_________1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "Prefetch_________2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + } + + /** + * Ensures that checkpoint dumps are used and written, by successively increasing the + * stub size and dumping until the duration crosses a threshold. + * + * @param $checkpointFormat string: Either "file" for plain text or "gzip" for gzipped + * checkpoint files. + */ + private function checkpointHelper( $checkpointFormat = "file" ) { + // Getting temporary names + $nameStub = $this->getNewTempFile(); + $nameOutputDir = $this->getNewTempDirectory(); + + $stderr = fopen( 'php://output', 'a' ); + if ( $stderr === FALSE ) { + $this->fail( "Could not open stream for stderr" ); + } + + $iterations = 32; // We'll start with that many iterations of revisions in stub + $lastDuration = 0; + $minDuration = 2; // We want the dump to take at least this many seconds + $checkpointAfter = 0.5; // Generate checkpoint after this many seconds + + + // Until a dump takes at least $minDuration seconds, perform a dump and check + // duration. If the dump did not take long enough increase the iteration + // count, to generate a bigger stub file next time. + while ( $lastDuration < $minDuration ) { + + // Setting up the dump + wfRecursiveRemoveDir( $nameOutputDir ); + $this->assertTrue( wfMkdirParents( $nameOutputDir ), + "Creating temporary output directory " ); + $this->setUpStub( $nameStub, $iterations ); + $dumper = new TextPassDumper( array ( "--stub=file:" . $nameStub, + "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full", + "--maxtime=1" /*This is in minutes. Fixup is below*/, + "--checkpointfile=checkpoint-%s-%s.xml.gz" ) ); + $dumper->setDb( $this->db ); + $dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute + $dumper->stderr = $stderr; + + // The actual dump and taking time + $ts_before = microtime( true ); + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + $ts_after = microtime( true ); + $lastDuration = $ts_after - $ts_before; + + // Handling increasing the iteration count for the stubs + if ( $lastDuration < $minDuration ) { + $old_iterations = $iterations; + if ( $lastDuration > 0.2 ) { + // lastDuration is big enough, to allow an educated guess + $factor = ( $minDuration + 0.5 ) / $lastDuration; + if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) { + // educated guess is reasonable + $iterations = (int)( $iterations * $factor ); + } + } + + if ( $old_iterations == $iterations ) { + // Heuristics were not applied, so we just *2. + $iterations *= 2; + } + + $this->assertLessThan( 50000, $iterations, + "Emergency stop against infinitely increasing iteration " + . "count ( last duration: $lastDuration )" ); + } + } + + // The dump (hopefully) did take long enough to produce more than one + // checkpoint file. + // + // We now check all the checkpoint files for validity. + + $files = scandir( $nameOutputDir ); + $this->assertTrue( asort( $files ), "Sorting files in temporary directory" ); + $fileOpened = false; + $lookingForPage = 1; + $checkpointFiles = 0; + + // Each run of the following loop body tries to handle exactly 1 /page/ (not + // iteration of stub content). $i is only increased after having treated page 4. + for ( $i = 0 ; $i < $iterations ; ) { + + // 1. Assuring a file is opened and ready. Skipping across header if + // necessary. + if ( ! $fileOpened ) { + $this->assertNotEmpty( $files, "No more existing dump files, " + . "but not yet all pages found" ); + $fname = array_shift( $files ); + while ( $fname == "." || $fname == ".." ) { + $this->assertNotEmpty( $files, "No more existing dump" + . " files, but not yet all pages found" ); + $fname = array_shift( $files ); + } + if ( $checkpointFormat == "gzip" ) { + $this->gunzip( $nameOutputDir . "/" . $fname ); + } + $this->assertDumpStart( $nameOutputDir . "/" . $fname ); + $fileOpened = true; + $checkpointFiles++; + } + + // 2. Performing a single page check + switch ( $lookingForPage ) { + case 1: + // Page 1 + $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 2; + break; + + case 2: + // Page 2 + $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", + $this->revId2_3 + $i * self::$numOfRevs ); + $this->assertPageEnd(); + + $lookingForPage = 4; + break; + + case 4: + // Page 4 + $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK, + "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 1; + + // We dealt with the whole iteration. + $i++; + break; + + default: + $this->fail( "Bad setting for lookingForPage ($lookingForPage)" ); + } + + // 3. Checking for the end of the current checkpoint file + if ( $this->xml->nodeType == XMLReader::END_ELEMENT + && $this->xml->name == "mediawiki" ) { + + $this->assertDumpEnd(); + $fileOpened = false; + } + } + + // Assuring we completely read all files ... + $this->assertFalse( $fileOpened, "Currently read file still open?" ); + $this->assertEmpty( $files, "Remaining unchecked files" ); + + // ... and have dealt with more than one checkpoint file + $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" ); + + $this->expectETAOutput(); + } + + /** + * @group large + */ + function testCheckpointPlain() { + $this->checkpointHelper(); + } + + /** + * tests for working checkpoint generation in gzip format work. + * + * We keep this test in addition to the simpler self::testCheckpointPlain, as there + * were once problems when the used sinks were DumpPipeOutputs. + * + * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires + * PHP extensions, we go for gzip instead, which triggers the same relevant code + * paths while still being testable on more systems. + * + * @group large + */ + function testCheckpointGzip() { + $this->checkpointHelper( "gzip" ); + } + + + /** + * Creates a stub file that is used for testing the text pass of dumps + * + * @param $fname string: (Optional) Absolute name of the file to write + * the stub into. If this parameter is null, a new temporary + * file is generated that is automatically removed upon + * tearDown. + * @param $iterations integer: (Optional) specifies how often the block + * of 3 pages should go into the stub file. The page and + * revision id increase further and further, while the text + * id of the first iteration is reused. The pages and revision + * of iteration > 1 have no corresponding representation in the + * database. + * @return string absolute filename of the stub + */ + private function setUpStub( $fname = null, $iterations = 1 ) { + if ( $fname === null ) { + $fname = $this->getNewTempFile(); + } + $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" ' + . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ ' + . 'http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.20alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + $tail = '</mediawiki> +'; + + $content = $header; + $iterations = intval( $iterations ); + for ( $i = 0; $i < $iterations; $i++ ) { + + $page1 = ' <page> + <title>BackupDumperTestP1</title> + <ns>0</ns> + <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP1Summary1</comment> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + <text id="' . $this->textId1_1 . '" bytes="23" /> + </revision> + </page> +'; + $page2 = ' <page> + <title>BackupDumperTestP2</title> + <ns>0</ns> + <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary1</comment> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + <text id="' . $this->textId2_1 . '" bytes="23" /> + </revision> + <revision> + <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary2</comment> + <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1> + <text id="' . $this->textId2_2 . '" bytes="23" /> + </revision> + <revision> + <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary3</comment> + <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1> + <text id="' . $this->textId2_3 . '" bytes="23" /> + </revision> + <revision> + <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary4 extra</comment> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + <text id="' . $this->textId2_4 . '" bytes="44" /> + </revision> + </page> +'; + // page 3 not in stub + + $page4 = ' <page> + <title>Talk:BackupDumperTestP1</title> + <ns>1</ns> + <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>Talk BackupDumperTestP1 Summary1</comment> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + <text id="' . $this->textId4_1 . '" bytes="35" /> + </revision> + </page> +'; + $content .= $page1 . $page2 . $page4; + } + $content .= $tail; + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared stub" ); + return $fname; + } + +} diff --git a/tests/phpunit/maintenance/backup_LogTest.php b/tests/phpunit/maintenance/backup_LogTest.php new file mode 100644 index 00000000..8a8dea5a --- /dev/null +++ b/tests/phpunit/maintenance/backup_LogTest.php @@ -0,0 +1,227 @@ +<?php +/** + * Tests for log dumps of BackupDumper + * + * @group Database + * @group Dump + */ +class BackupDumperLoggerTest extends DumpTestCase { + + + // We'll add several log entries and users for this test. The following + // variables hold the corresponding ids. + private $userId1, $userId2; + private $logId1, $logId2, $logId3; + + /** + * adds a log entry to the database. + * + * @param $type string: type of the log entry + * @param $subtype string: subtype of the log entry + * @param $user User: user that performs the logged operation + * @param $ns int: number of the namespace for the entry's target's title + * @param $title string: title of the entry's target + * @param $comment string: comment of the log entry + * @param $parameters Array: (optional) accompanying data that is attached + * to the entry + * + * @return int id of the added log entry + */ + private function addLogEntry( $type, $subtype, User $user, $ns, $title, + $comment = null, $parameters = null ) { + + $logEntry = new ManualLogEntry( $type, $subtype ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( Title::newFromText( $title, $ns ) ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + if ( $parameters !== null ) { + $logEntry->setParameters( $parameters ); + } + return $logEntry->insert(); + } + + function addDBData() { + $this->tablesUsed[] = 'logging'; + $this->tablesUsed[] = 'user'; + + try { + $user1 = User::newFromName( 'BackupDumperLogUserA' ); + $this->userId1 = $user1->getId(); + if ( $this->userId1 === 0 ) { + $user1->addToDatabase(); + $this->userId1 = $user1->getId(); + } + $this->assertGreaterThan( 0, $this->userId1 ); + + $user2 = User::newFromName( 'BackupDumperLogUserB' ); + $this->userId2 = $user2->getId(); + if ( $this->userId2 === 0 ) { + $user2->addToDatabase(); + $this->userId2 = $user2->getId(); + } + $this->assertGreaterThan( 0, $this->userId2 ); + + $this->logId1 = $this->addLogEntry( 'type', 'subtype', + $user1, NS_MAIN, "PageA" ); + $this->assertGreaterThan( 0, $this->logId1 ); + + $this->logId2 = $this->addLogEntry( 'supress', 'delete', + $user2, NS_TALK, "PageB", "SomeComment" ); + $this->assertGreaterThan( 0, $this->logId2 ); + + $this->logId3 = $this->addLogEntry( 'move', 'delete', + $user2, NS_MAIN, "PageA", "SomeOtherComment", + array( 'key1' => 1, 3 => 'value3' ) ); + $this->assertGreaterThan( 0, $this->logId3 ); + + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + + } + + + /** + * asserts that the xml reader is at the beginning of a log entry and skips over + * it while analyzing it. + * + * @param $id int: id of the log entry + * @param $user_name string: user name of the log entry's performer + * @param $user_id int: user id of the log entry 's performer + * @param $comment string|null: comment of the log entry. If null, the comment + * text is ignored. + * @param $type string: type of the log entry + * @param $subtype string: subtype of the log entry + * @param $title string: title of the log entry's target + * @param $parameters array: (optional) unserialized data accompanying the log entry + */ + private function assertLogItem( $id, $user_name, $user_id, $comment, $type, + $subtype, $title, $parameters = array() ) { + + $this->assertNodeStart( "logitem" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "username", $user_name ); + $this->assertTextNode( "id", $user_id ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + if ( $comment !== null ) { + $this->assertTextNode( "comment", $comment ); + } + $this->assertTextNode( "type", $type ); + $this->assertTextNode( "action", $subtype ); + $this->assertTextNode( "logtitle", $title ); + + $this->assertNodeStart( "params" ); + $parameters_xml = unserialize( $this->xml->value ); + $this->assertEquals( $parameters, $parameters_xml ); + $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" ); + $this->assertNodeEnd( "params" ); + $this->skipWhitespace(); + + $this->assertNodeEnd( "logitem" ); + $this->skipWhitespace(); + } + + function testPlain () { + global $wgContLang; + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array ( "--output=file:" . $fname ) ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + // Analyzing the dumped data + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + } + + function testXmlDumpsBackupUseCaseLogging() { + global $wgContLang; + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array ( "--output=gzip:" . $fname, + "--reporting=2" ) ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->setDb( $this->db ); + + // xmldumps-backup demands reporting, although this is currently not + // implemented in BackupDumper, when dumping logging data. We + // nevertheless capture the output of the dump process already now, + // to be able to alert (once dumping produces reports) that this test + // needs updates. + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === FALSE ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Analyzing the dumped data + $this->gunzip( $fname ); + + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + + // Currently, no reporting is implemented. Alert via failure, once + // this changes. + // If reporting for log dumps has been implemented, please update + // the following statement to catch good output + $this->expectOutputString( '' ); + } + +} diff --git a/tests/phpunit/maintenance/backup_PageTest.php b/tests/phpunit/maintenance/backup_PageTest.php new file mode 100644 index 00000000..925e277d --- /dev/null +++ b/tests/phpunit/maintenance/backup_PageTest.php @@ -0,0 +1,389 @@ +<?php +/** + * Tests for page dumps of BackupDumper + * + * @group Database + * @group Dump + */ +class BackupDumperPageTest extends DumpTestCase { + + // We'll add several pages, revision and texts. The following variables hold the + // corresponding ids. + private $pageId1, $pageId2, $pageId3, $pageId4, $pageId5; + private $revId1_1, $textId1_1; + private $revId2_1, $textId2_1, $revId2_2, $textId2_2; + private $revId2_3, $textId2_3, $revId2_4, $textId2_4; + private $revId3_1, $textId3_1, $revId3_2, $textId3_2; + private $revId4_1, $textId4_1; + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + $title = Title::newFromText( 'BackupDumperTestP1' ); + $page = WikiPage::factory( $title ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + $title = Title::newFromText( 'BackupDumperTestP2' ); + $page = WikiPage::factory( $title ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + $title = Title::newFromText( 'BackupDumperTestP3' ); + $page = WikiPage::factory( $title ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK ); + $page = WikiPage::factory( $title ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + + } + + public function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + + } + + function testFullTextPlain () { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array ( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testFullStubPlain () { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array ( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubPlain () { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array ( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubGzip () { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array ( "--output=gzip:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->gunzip( $fname ); + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + + + function testXmlDumpsBackupUseCase () { + // xmldumps-backup typically performs a single dump that that writes + // out three files + // * gzipped stubs of everything (meta-history) + // * gzipped stubs of latest revisions of all pages (meta-current) + // * gzipped stubs of latest revisions of all pages of namespage 0 + // (articles) + // + // We reproduce such a setup with our mini fixture, although we omit + // chunks, and all the other gimmicks of xmldumps-backup. + // + $fnameMetaHistory = $this->getNewTempFile(); + $fnameMetaCurrent = $this->getNewTempFile(); + $fnameArticles = $this->getNewTempFile(); + + $dumper = new BackupDumper( array ( "--output=gzip:" . $fnameMetaHistory, + "--output=gzip:" . $fnameMetaCurrent, "--filter=latest", + "--output=gzip:" . $fnameArticles, "--filter=latest", + "--filter=notalk", "--filter=namespace:!NS_USER", + "--reporting=1000" ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->setDb( $this->db ); + + // xmldumps-backup uses reporting. We will not check the exact reported + // message, as they are dependent on the processing power of the used + // computer. We only check that reporting does not crash the dumping + // and that something is reported + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === FALSE ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Checking meta-history ------------------------------------------------- + + $this->gunzip( $fnameMetaHistory ); + $this->assertDumpStart( $fnameMetaHistory ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking meta-current ------------------------------------------------- + + $this->gunzip( $fnameMetaCurrent ); + $this->assertDumpStart( $fnameMetaCurrent ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking articles ------------------------------------------------- + + $this->gunzip( $fnameArticles ); + $this->assertDumpStart( $fnameArticles ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + // -> Page is not in NS_MAIN. Hence not visible + + $this->assertDumpEnd(); + + $this->expectETAOutput(); + } + + + +} diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php new file mode 100644 index 00000000..e7ffa01c --- /dev/null +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -0,0 +1,243 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/fetchText.php"; + +/** + * Mock for the input/output of FetchText + * + * FetchText internally tries to access stdin and stdout. We mock those aspects + * for testing. + */ +class SemiMockedFetchText extends FetchText { + + /** + * @var String|null Text to pass as stdin + */ + private $mockStdinText = null; + + /** + * @var bool Whether or not a text for stdin has been provided + */ + private $mockSetUp = False; + + /** + * @var Array Invocation counters for the mocked aspects + */ + private $mockInvocations = array( 'getStdin' => 0 ); + + + + /** + * Data for the fake stdin + * + * @param $stdin String The string to be used instead of stdin + */ + function mockStdin( $stdin ) + { + $this->mockStdinText = $stdin; + $this->mockSetUp = True; + } + + /** + * Gets invocation counters for mocked methods. + * + * @return Array An array, whose keys are function names. The corresponding values + * denote the number of times the function has been invoked. + */ + function mockGetInvocations() + { + return $this->mockInvocations; + } + + // ----------------------------------------------------------------- + // Mocked functions from FetchText follow. + + function getStdin( $len = null ) + { + $this->mockInvocations['getStdin']++; + if ( $len !== null ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin with non null parameter" ); + } + + if ( ! $this->mockSetUp ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin before setting up rerouting" ); + } + + return fopen( 'data://text/plain,' . $this->mockStdinText, 'r' ); + } + +} + +/** + * TestCase for FetchText + * + * @group Database + * @group Dump + */ +class FetchTextTest extends MediaWikiTestCase { + + // We add 5 Revisions for this test. Their corresponding text id's + // are stored in the following 5 variables. + private $textId1; + private $textId2; + private $textId3; + private $textId4; + private $textId5; + + + /** + * @var Exception|null As the current MediaWikiTestCase::run is not + * robust enough to recover from thrown exceptions directly, we cannot + * throw frow within addDBData, although it would be appropriate. Hence, + * we catch the exception and store it until we are in setUp and may + * finally rethrow the exception without crashing the test suite. + */ + private $exceptionFromAddDBData; + + /** + * @var FetchText the (mocked) FetchText that is to test + */ + private $fetchText; + + /** + * Adds a revision to a page, while returning the resuting text's id + * + * @param $page WikiPage The page to add the revision to + * @param $text String The revisions text + * @param $text String The revisions summare + * + * @throws MWExcepion + */ + private function addRevision( $page, $text, $summary ) { + $status = $page->doEdit( $text, $summary ); + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $id = $revision->getTextId(); + if ( $id > 0 ) { + return $id; + } + } + throw new MWException( "Could not determine text id" ); + } + + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + $title = Title::newFromText( 'FetchTextTestPage1' ); + $page = WikiPage::factory( $title ); + $this->textId1 = $this->addRevision( $page, "FetchTextTestPage1Text1", "FetchTextTestPage1Summary1" ); + + $title = Title::newFromText( 'FetchTextTestPage2' ); + $page = WikiPage::factory( $title ); + $this->textId2 = $this->addRevision( $page, "FetchTextTestPage2Text1", "FetchTextTestPage2Summary1" ); + $this->textId3 = $this->addRevision( $page, "FetchTextTestPage2Text2", "FetchTextTestPage2Summary2" ); + $this->textId4 = $this->addRevision( $page, "FetchTextTestPage2Text3", "FetchTextTestPage2Summary3" ); + $this->textId5 = $this->addRevision( $page, "FetchTextTestPage2Text4 some additional Text ", "FetchTextTestPage2Summary4 extra " ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData + $this->exceptionFromAddDBData = $e; + } + } + + + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $this->fetchText = new SemiMockedFetchText(); + } + + + /** + * Helper to relate FetchText's input and output + */ + private function assertFilter( $input, $expectedOutput ) { + $this->fetchText->mockStdin( $input ); + $this->fetchText->execute(); + $invocations = $this->fetchText->mockGetInvocations(); + $this->assertEquals( 1, $invocations['getStdin'], + "getStdin invocation counter" ); + $this->expectOutputString( $expectedOutput ); + } + + + + // Instead of the following functions, a data provider would be great. + // However, as data providers are evaluated /before/ addDBData, a data + // provider would not know the required ids. + + function testExistingSimple() { + $this->assertFilter( $this->textId2, + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSimpleWithNewline() { + $this->assertFilter( $this->textId2 . "\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSeveral() { + $this->assertFilter( "$this->textId1\n$this->textId5\n" + . "$this->textId3\n$this->textId3", + implode( "", array( + $this->textId1 . "\n23\nFetchTextTestPage1Text1", + $this->textId5 . "\n44\nFetchTextTestPage2Text4 " + . "some additional Text", + $this->textId3 . "\n23\nFetchTextTestPage2Text2", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } + + function testEmpty() { + $this->assertFilter( "", null ); + } + + function testNonExisting() { + $this->assertFilter( $this->textId5 + 10, ( $this->textId5 + 10 ) . "\n-1\n" ); + } + + function testNegativeInteger() { + $this->assertFilter( "-42", "-42\n-1\n" ); + } + + function testFloatingPointNumberExisting() { + // float -> int -> revision + $this->assertFilter( $this->textId3 + 0.14159, + $this->textId3 . "\n23\nFetchTextTestPage2Text2" ); + } + + function testFloatingPointNumberNonExisting() { + $this->assertFilter( $this->textId5 + 3.14159, + ( $this->textId5 + 3 ) . "\n-1\n" ); + } + + function testCharacters() { + $this->assertFilter( "abc", "0\n-1\n" ); + } + + function testMix() { + $this->assertFilter( "ab\n" . $this->textId4 . ".5cd\n\nefg\n" . $this->textId2 + . "\n" . $this->textId3, + implode( "", array( + "0\n-1\n", + $this->textId4 . "\n23\nFetchTextTestPage2Text3", + "0\n-1\n", + "0\n-1\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } + +} diff --git a/tests/phpunit/maintenance/getSlaveServerTest.php b/tests/phpunit/maintenance/getSlaveServerTest.php new file mode 100644 index 00000000..0b7c758c --- /dev/null +++ b/tests/phpunit/maintenance/getSlaveServerTest.php @@ -0,0 +1,69 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/getSlaveServer.php"; + +/** + * Tests for getSlaveServer + * + * @group Database + */ +class GetSlaveServerTest extends MediaWikiTestCase { + + /** + * Yields a regular expression that matches a good DB server name + * + * It matches IPs or hostnames, both optionally followed by a + * port specification + * + * @return String the regular expression + */ + private function getServerRE() { + if ( $this->db->getType() === 'sqlite' ) { + // for SQLite, only the empty string is a good server name + return ''; + } + + $octet = '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; + $ip = "(($octet\.){3}$octet)"; + + $label = '([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)'; + $hostname = "($label(\.$label)*)"; + + return "($ip|$hostname)(:[0-9]{1,5})?"; + } + + function testPlain() { + $gss = new GetSlaveServer(); + $gss->execute(); + + $this->expectOutputRegex( "/^" . self::getServerRE() . "\n$/D" ); + } + + function testXmlDumpsBackupUseCase() { + global $wgDBprefix; + + global $argv; + $argv = array( null, "--globals" ); + + $gss = new GetSlaveServer(); + $gss->loadParamsAndArgs(); + $gss->execute(); + $gss->globals(); + + // The main answer + $output = $this->getActualOutput(); + $firstLineEndPos = strpos( $output,"\n"); + if ( $firstLineEndPos === FALSE ) { + $this->fail( "Could not find end of first line of output" ); + } + $firstLine = substr( $output, 0 , $firstLineEndPos ); + $this->assertRegExp( "/^" . self::getServerRE() . "$/D", + $firstLine, "DB Server" ); + + // xmldumps-backup relies on the wgDBprefix in the output. + $this->expectOutputRegex( "/^[[:space:]]*\[wgDBprefix\][[:space:]]*=> " + . $wgDBprefix . "$/m" ); + } + + +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php index 92eeffa2..bcbf4ec1 100644 --- a/tests/phpunit/phpunit.php +++ b/tests/phpunit/phpunit.php @@ -9,7 +9,7 @@ /* Configuration */ // Evaluate the include path relative to this file -$IP = dirname( dirname( dirname( __FILE__ ) ) ); +$IP = dirname( dirname( __DIR__ ) ); // Set a flag which can be used to detect when other scripts have been entered through this entry point or not define( 'MW_PHPUNIT_TEST', true ); @@ -18,15 +18,31 @@ define( 'MW_PHPUNIT_TEST', true ); require_once( "$IP/maintenance/Maintenance.php" ); class PHPUnitMaintClass extends Maintenance { + + function __construct() { + parent::__construct(); + $this->addOption( 'with-phpunitdir' + , 'Directory to include PHPUnit from, for example when using a git fetchout from upstream. Path will be prepended to PHP `include_path`.' + , false # not required + , true # need arg + ); + } + public function finalSetup() { parent::finalSetup(); - global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgUseDatabaseMessages; + global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; + global $wgLanguageConverterCacheType, $wgUseDatabaseMessages; global $wgLocaltimezone, $wgLocalisationCacheConf; + global $wgDevelopmentWarnings; + + // wfWarn should cause tests to fail + $wgDevelopmentWarnings = true; $wgMainCacheType = CACHE_NONE; $wgMessageCacheType = CACHE_NONE; $wgParserCacheType = CACHE_NONE; + $wgLanguageConverterCacheType = CACHE_NONE; $wgUseDatabaseMessages = false; # Set for future resets @@ -35,7 +51,42 @@ class PHPUnitMaintClass extends Maintenance { $wgLocalisationCacheConf['storeClass'] = 'LCStore_Null'; } - public function execute() { } + + public function execute() { + global $IP; + + # Make sure we have --configuration or PHPUnit might complain + if( !in_array( '--configuration', $_SERVER['argv'] ) ) { + //Hack to eliminate the need to use the Makefile (which sucks ATM) + array_splice( $_SERVER['argv'], 1, 0, + array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) ); + } + + # --with-phpunitdir let us override the default PHPUnit version + if( $phpunitDir = $this->getOption( 'with-phpunitdir' ) ) { + # Sanity checks + if( !is_dir($phpunitDir) ) { + $this->error( "--with-phpunitdir should be set to an existing directory", 1 ); + } + if( !is_readable( $phpunitDir."/PHPUnit/Runner/Version.php" ) ) { + $this->error( "No usable PHPUnit installation in $phpunitDir.\nAborting.\n", 1 ); + } + + # Now prepends provided PHPUnit directory + $this->output( "Will attempt loading PHPUnit from `$phpunitDir`\n" ); + set_include_path( $phpunitDir + . PATH_SEPARATOR . get_include_path() ); + + # Cleanup $args array so the option and its value do not + # pollute PHPUnit + $key = array_search( '--with-phpunitdir', $_SERVER['argv'] ); + unset( $_SERVER['argv'][$key] ); // the option + unset( $_SERVER['argv'][$key+1] ); // its value + $_SERVER['argv'] = array_values( $_SERVER['argv'] ); + + } + } + public function getDbType() { return Maintenance::DB_ADMIN; } @@ -44,18 +95,13 @@ class PHPUnitMaintClass extends Maintenance { $maintClass = 'PHPUnitMaintClass'; require( RUN_MAINTENANCE_IF_MAIN ); -if( !in_array( '--configuration', $_SERVER['argv'] ) ) { - //Hack to eliminate the need to use the Makefile (which sucks ATM) - array_splice( $_SERVER['argv'], 1, 0, - array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) ); -} - require_once( 'PHPUnit/Runner/Version.php' ); -if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) { - die( 'PHPUnit 3.5 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" ); + +if( PHPUnit_Runner_Version::id() !== '@package_version@' + && version_compare( PHPUnit_Runner_Version::id(), '3.6.7', '<' ) ) { + die( 'PHPUnit 3.6.7 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" ); } require_once( 'PHPUnit/Autoload.php' ); require_once( "$IP/tests/TestsAutoLoader.php" ); MediaWikiPHPUnitCommand::main(); - diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml index 1227a17a..f286fa11 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -23,6 +23,11 @@ <testsuite name="skins"> <directory>skins</directory> </testsuite> + <!-- As there is a class Maintenance, we cannot use the + name "maintenance" directly --> + <testsuite name="maintenance_suite"> + <directory>maintenance</directory> + </testsuite> <testsuite name="structure"> <file>StructureTest.php</file> </testsuite> diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php index 6779ad47..f2638111 100644 --- a/tests/phpunit/suites/UploadFromUrlTestSuite.php +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -1,6 +1,6 @@ <?php -require_once( dirname( dirname( __FILE__ ) ) . '/includes/upload/UploadFromUrlTest.php' ); +require_once( dirname( __DIR__ ) . '/includes/upload/UploadFromUrlTest.php' ); class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { public $savedGlobals = array(); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 670e3d11..59ae73cd 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -19,13 +19,18 @@ return array( 'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js', 'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js', 'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js', + 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js', + 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js', 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js', - "tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js", + 'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js', ), 'dependencies' => array( 'jquery.autoEllipsis', @@ -42,11 +47,17 @@ return array( 'jquery.tablesorter', 'jquery.textSelection', 'mediawiki', + 'mediawiki.api', + 'mediawiki.api.parse', + 'mediawiki.jqueryMsg', 'mediawiki.Title', + 'mediawiki.Uri', 'mediawiki.user', 'mediawiki.util', 'mediawiki.special.recentchanges', - 'mediawiki.jqueryMsg', + 'mediawiki.language', + 'mediawiki.cldr', ), + 'position' => 'top', ) ); diff --git a/tests/qunit/data/callMwLoaderTestCallback.js b/tests/qunit/data/callMwLoaderTestCallback.js new file mode 100644 index 00000000..3f2ee92f --- /dev/null +++ b/tests/qunit/data/callMwLoaderTestCallback.js @@ -0,0 +1 @@ +mw.loader.testCallback(); diff --git a/tests/qunit/data/defineTestCallback.js b/tests/qunit/data/defineTestCallback.js deleted file mode 100644 index 6fcd4595..00000000 --- a/tests/qunit/data/defineTestCallback.js +++ /dev/null @@ -1,4 +0,0 @@ -window.mw.loader.testCallback = function() { - start(); - ok( true, 'Implementing a module, is the callback timed properly ?'); -}; diff --git a/tests/qunit/data/load.mock.php b/tests/qunit/data/load.mock.php new file mode 100644 index 00000000..1c189703 --- /dev/null +++ b/tests/qunit/data/load.mock.php @@ -0,0 +1,58 @@ +<?php +/** + * Mock load.php with pre-defined test modules. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @package MediaWiki + * @author Lupo + * @since 1.20 + */ +header( 'Content-Type: text/javascript; charset=utf-8' ); + +require_once '../../../includes/Xml.php'; + +$moduleImplementations = array( + 'testUsesMissing' => " +mw.loader.implement( 'testUsesMissing', function () { + QUnit.ok( false, 'Module test.usesMissing script should not run.'); + QUnit.start(); +}, {}, {}); +", + + 'testUsesNestedMissing' => " +mw.loader.implement( 'testUsesNestedMissing', function () { + QUnit.ok( false, 'Module testUsesNestedMissing script should not run.'); +}, {}, {}); +", +); + +$response = ''; + +// Only support for non-encoded module names, full module names expected +if ( isset( $_GET['modules'] ) ) { + $modules = explode( ',', $_GET['modules'] ); + foreach ( $modules as $module ) { + if ( isset( $moduleImplementations[$module] ) ) { + $response .= $moduleImplementations[$module]; + } else { + $response .= Xml::encodeJsCall( 'mw.loader.state', array( $module, 'missing' ) ); + } + } +} + +echo $response; diff --git a/tests/qunit/data/qunitOkCall.js b/tests/qunit/data/qunitOkCall.js index 2fb6e01d..25c42d6a 100644 --- a/tests/qunit/data/qunitOkCall.js +++ b/tests/qunit/data/qunitOkCall.js @@ -1,2 +1,2 @@ -start(); -ok( true, 'Successfully loaded!'); +QUnit.start(); +QUnit.assert.ok( true, 'Successfully loaded!'); diff --git a/tests/qunit/data/styleTest.css.php b/tests/qunit/data/styleTest.css.php new file mode 100644 index 00000000..1870d5a3 --- /dev/null +++ b/tests/qunit/data/styleTest.css.php @@ -0,0 +1,61 @@ +<?php +/** + * Dynamically create a simple stylesheet for unit tests in MediaWiki. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @package MediaWiki + * @author Timo Tijhof + * @since 1.20 + */ +header( 'Content-Type: text/css; charset=utf-8' ); + +/** + * Allows characters in ranges [a-z], [A-Z] and [0-9], + * in addition to a dot ("."), dash ("-"), space (" ") and hash ("#"). + * @since 1.20 + * + * @param string $val + * @return string Value with any illegal characters removed. + */ +function cssfilter( $val ) { + return preg_replace( '/[^A-Za-z0-9\.\- #]/', '', $val ); +} + +// Do basic sanitization +$params = array_map( 'cssfilter', $_GET ); + +// Defaults +$selector = isset( $params['selector'] ) ? $params['selector'] : '.mw-test-example'; +$property = isset( $params['prop'] ) ? $params['prop'] : 'float'; +$value = isset( $params['val'] ) ? $params['val'] : 'right'; +$wait = isset( $params['wait'] ) ? (int)$params['wait'] : 0; // seconds + +sleep( $wait ); + +$css = " +/** + * Generated " . gmdate( 'r' ) . ". + * Waited {$wait}s. + */ + +$selector { + $property: $value; +} +"; + +echo trim( $css ) . "\n"; diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js index fdd3116b..efa65493 100644 --- a/tests/qunit/data/testrunner.js +++ b/tests/qunit/data/testrunner.js @@ -1,44 +1,61 @@ ( function ( $, mw, QUnit, undefined ) { -"use strict"; +/*global CompletenessTest */ +/*jshint evil:true */ +'use strict'; var mwTestIgnore, mwTester, addons; /** * Add bogus to url to prevent IE crazy caching * - * @param value {String} a relative path (eg. 'data/defineTestCallback.js' + * @param value {String} a relative path (eg. 'data/foo.js' * or 'data/test.php?foo=bar'). - * @return {String} Such as 'data/defineTestCallback.js?131031765087663960' + * @return {String} Such as 'data/foo.js?131031765087663960' */ -QUnit.fixurl = function (value) { +QUnit.fixurl = function ( value ) { return value + (/\?/.test( value ) ? '&' : '?') + String( new Date().getTime() ) - + String( parseInt( Math.random()*100000, 10 ) ); + + String( parseInt( Math.random() * 100000, 10 ) ); }; /** * Configuration */ -QUnit.config.testTimeout = 5000; -/** - * MediaWiki debug mode - */ -QUnit.config.urlConfig.push( 'debug' ); +// When a test() indicates asynchronicity with stop(), +// allow 10 seconds to pass before killing the test(), +// and assuming failure. +QUnit.config.testTimeout = 10 * 1000; + +// Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode. +QUnit.config.urlConfig.push( { + id: 'debug', + label: 'Enable ResourceLoaderDebug', + tooltip: 'Enable debug mode in ResourceLoader' +} ); /** - * Load TestSwarm agent + * Load TestSwarm agent */ -if ( QUnit.urlParams.swarmURL ) { - document.write( "<scr" + "ipt src='" + QUnit.fixurl( mw.config.get( 'wgScriptPath' ) - + '/tests/qunit/data/testwarm.inject.js' ) + "'></scr" + "ipt>" ); +// Only if the current url indicates that there is a TestSwarm instance watching us +// (TestSwarm appends swarmURL to the test suites url it loads in iframes). +// Otherwise this is just a simple view of Special:JavaScriptTest/qunit directly, +// no point in loading inject.js in that case. Also, make sure that this instance +// of MediaWiki has actually been configured with the required url to that inject.js +// script. By default it is false. +if ( QUnit.urlParams.swarmURL && mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) { + document.write( "<scr" + "ipt src='" + QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) + "'></scr" + "ipt>" ); } /** * CompletenessTest */ // Adds toggle checkbox to header -QUnit.config.urlConfig.push( 'completenesstest' ); +QUnit.config.urlConfig.push( { + id: 'completenesstest', + label: 'Run CompletenessTest', + tooltip: 'Run the completeness test' +} ); // Initiate when enabled if ( QUnit.urlParams.completenesstest ) { @@ -77,70 +94,87 @@ if ( QUnit.urlParams.completenesstest ) { QUnit.config.urlConfig.push( 'mwlogenv' ); /** - * Reset mw.config to a fresh copy of the live config for each test(); - * @param override {Object} [optional] - * @example: - * <code> - * module( .., newMwEnvironment() ); - * - * test( .., function () { - * mw.config.set( 'foo', 'bar' ); // just for this test - * } ); - * - * test( .., function () { - * mw.config.get( 'foo' ); // doesn't exist - * } ); - * - * - * module( .., newMwEnvironment({ quux: 'corge' }) ); - * - * test( .., function () { - * mw.config.get( 'quux' ); // "corge" - * mw.config.set( 'quux', "grault" ); - * } ); - * - * test( .., function () { - * mw.config.get( 'quux' ); // "corge" - * } ); + * Reset mw.config and others to a fresh copy of the live config for each test(), + * and restore it back to the live one afterwards. + * @param localEnv {Object} [optional] + * @example (see test suite at the bottom of this file) * </code> */ QUnit.newMwEnvironment = ( function () { - var liveConfig, freshConfigCopy, log; + var log, liveConfig, liveMessages; liveConfig = mw.config.values; + liveMessages = mw.messages.values; - freshConfigCopy = function ( custom ) { + function freshConfigCopy( custom ) { // "deep=true" is important here. // Otherwise we just create a new object with values referring to live config. // e.g. mw.config.set( 'wgFileExtensions', [] ) would not effect liveConfig, // but mw.config.get( 'wgFileExtensions' ).push( 'png' ) would as the array // was passed by reference in $.extend's loop. - return $.extend({}, liveConfig, custom, /*deep=*/true ); - }; + return $.extend( {}, liveConfig, custom, /*deep=*/true ); + } + + function freshMessagesCopy( custom ) { + return $.extend( {}, liveMessages, custom, /*deep=*/true ); + } log = QUnit.urlParams.mwlogenv ? mw.log : function () {}; - return function ( override ) { - override = override || {}; + return function ( localEnv ) { + localEnv = $.extend( { + // QUnit + setup: $.noop, + teardown: $.noop, + // MediaWiki + config: {}, + messages: {} + }, localEnv ); return { setup: function () { log( 'MwEnvironment> SETUP for "' + QUnit.config.current.module + ': ' + QUnit.config.current.testName + '"' ); - // Greetings, mock configuration! - mw.config.values = freshConfigCopy( override ); + + // Greetings, mock environment! + mw.config.values = freshConfigCopy( localEnv.config ); + mw.messages.values = freshMessagesCopy( localEnv.messages ); + + localEnv.setup(); }, teardown: function () { log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module + ': ' + QUnit.config.current.testName + '"' ); - // Farewell, mock configuration! + + localEnv.teardown(); + + // Farewell, mock environment! mw.config.values = liveConfig; + mw.messages.values = liveMessages; } }; }; }() ); +// $.when stops as soon as one fails, which makes sense in most +// practical scenarios, but not in a unit test where we really do +// need to wait until all of them are finished. +QUnit.whenPromisesComplete = function () { + var altPromises = []; + + $.each( arguments, function ( i, arg ) { + var alt = $.Deferred(); + altPromises.push( alt ); + + // Whether this one fails or not, forwards it to + // the 'done' (resolve) callback of the alternative promise. + arg.always( alt.resolve ); + }); + + return $.when.apply( $, altPromises ); +}; + /** * Add-on assertion helpers */ @@ -149,12 +183,12 @@ addons = { // Expect boolean true assertTrue: function ( actual, message ) { - strictEqual( actual, true, message ); + QUnit.push( actual === true, actual, true, message ); }, // Expect boolean false assertFalse: function ( actual, message ) { - strictEqual( actual, false, message ); + QUnit.push( actual === false, actual, false, message ); }, // Expect numerical value less than X @@ -175,14 +209,58 @@ addons = { // Expect numerical value greater than or equal to X gtOrEq: function ( actual, expected, message ) { QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message ); + } +}; + +$.extend( QUnit.assert, addons ); + +/** + * Small test suite to confirm proper functionality of the utilities and + * initializations in this file. + */ +var envExecCount = 0; +QUnit.module( 'mediawiki.tests.qunit.testrunner', QUnit.newMwEnvironment({ + setup: function () { + envExecCount += 1; + this.mwHtmlLive = mw.html; + mw.html = { + escape: function () { + return 'mocked-' + envExecCount; + } + }; + }, + teardown: function () { + mw.html = this.mwHtmlLive; + }, + config: { + testVar: 'foo' }, + messages: { + testMsg: 'Foo.' + } +}) ); - // Backwards compatible with new verions of QUnit - equals: window.equal, - same: window.deepEqual -}; +QUnit.test( 'Setup', 3, function ( assert ) { + assert.equal( mw.html.escape( 'foo' ), 'mocked-1', 'extra setup() callback was ran.' ); + assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' ); + assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' ); + + mw.config.set( 'testVar', 'bar' ); + mw.messages.set( 'testMsg', 'Bar.' ); +}); + +QUnit.test( 'Teardown', 3, function ( assert ) { + assert.equal( mw.html.escape( 'foo' ), 'mocked-2', 'extra setup() callback was re-ran.' ); + assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' ); + assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' ); +}); + +QUnit.module( 'mediawiki.tests.qunit.testrunner-after', QUnit.newMwEnvironment() ); -$.extend( QUnit, addons ); -$.extend( window, addons ); +QUnit.test( 'Teardown', 3, function ( assert ) { + assert.equal( mw.html.escape( '<' ), '<', 'extra teardown() callback was ran.' ); + assert.equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' ); + assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' ); +}); -})( jQuery, mediaWiki, QUnit ); +}( jQuery, mediaWiki, QUnit ) ); diff --git a/tests/qunit/data/testwarm.inject.js b/tests/qunit/data/testwarm.inject.js deleted file mode 100644 index 14ee8f93..00000000 --- a/tests/qunit/data/testwarm.inject.js +++ /dev/null @@ -1,349 +0,0 @@ -/* - Copyright (c) 2009 John Resig - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - -*/ -(function(){ - - var DEBUG = false; - - var doPost = false; - - try { - doPost = !!window.top.postMessage; - } catch(e){} - - var search = window.location.search, - url, index; - if( ( index = search.indexOf( "swarmURL=" ) ) != -1 ) - url = decodeURIComponent( search.slice( index + 9 ) ); - - if ( !DEBUG && (!url || url.indexOf("http") !== 0) ) { - return; - } - - var submitTimeout = 5; - - var curHeartbeat; - var beatRate = 20; - - // Expose the TestSwarm API - window.TestSwarm = { - submit: submit, - heartbeat: function(){ - if ( curHeartbeat ) { - clearTimeout( curHeartbeat ); - } - - curHeartbeat = setTimeout(function(){ - submit({ fail: -1, total: -1 }); - }, beatRate * 1000); - }, - serialize: function(){ - return trimSerialize(); - } - }; - - // Prevent careless things from executing - window.print = window.confirm = window.alert = window.open = function(){}; - - window.onerror = function(e){ - document.body.appendChild( document.createTextNode( "ERROR: " + e )); - submit({ fail: 0, error: 1, total: 1 }); - return false; - }; - - // QUnit (jQuery) - // http://docs.jquery.com/QUnit - if ( typeof QUnit !== "undefined" ) { - QUnit.done = function(results){ - submit({ - fail: results.failed, - error: 0, - total: results.total - }); - }; - - QUnit.log = window.TestSwarm.heartbeat; - window.TestSwarm.heartbeat(); - - window.TestSwarm.serialize = function(){ - // Clean up the HTML (remove any un-needed test markup) - remove("nothiddendiv"); - remove("loadediframe"); - remove("dl"); - remove("main"); - - // Show any collapsed results - var ol = document.getElementsByTagName("ol"); - for ( var i = 0; i < ol.length; i++ ) { - ol[i].style.display = "block"; - } - - return trimSerialize(); - }; - - // UnitTestJS (Prototype, Scriptaculous) - // http://github.com/tobie/unittest_js/tree/master - } else if ( typeof Test !== "undefined" && Test && Test.Unit && Test.Unit.runners ) { - var total_runners = Test.Unit.runners.length, cur_runners = 0; - var total = 0, fail = 0, error = 0; - - for (var i = 0; i < Test.Unit.runners.length; i++) (function(i){ - var finish = Test.Unit.runners[i].finish; - Test.Unit.runners[i].finish = function(){ - finish.call( this ); - - var results = this.getResult(); - total += results.assertions; - fail += results.failures; - error += results.errors; - - if ( ++cur_runners === total_runners ) { - submit({ - fail: fail, - error: error, - total: total - }); - } - }; - })(i); - - // JSSpec (MooTools) - // http://jania.pe.kr/aw/moin.cgi/JSSpec - } else if ( typeof JSSpec !== "undefined" && JSSpec && JSSpec.Logger ) { - var onRunnerEnd = JSSpec.Logger.prototype.onRunnerEnd; - JSSpec.Logger.prototype.onRunnerEnd = function(){ - onRunnerEnd.call(this); - - // Show any collapsed results - var ul = document.getElementsByTagName("ul"); - for ( var i = 0; i < ul.length; i++ ) { - ul[i].style.display = "block"; - } - - submit({ - fail: JSSpec.runner.getTotalFailures(), - error: JSSpec.runner.getTotalErrors(), - total: JSSpec.runner.totalExamples - }); - }; - - window.TestSwarm.serialize = function(){ - // Show any collapsed results - var ul = document.getElementsByTagName("ul"); - for ( var i = 0; i < ul.length; i++ ) { - ul[i].style.display = "block"; - } - - return trimSerialize(); - }; - - // JSUnit - // http://www.jsunit.net/ - // Note: Injection file must be included before the frames - // are document.write()d into the page. - } else if ( typeof JsUnitTestManager !== "undefined" ) { - var _done = JsUnitTestManager.prototype._done; - JsUnitTestManager.prototype._done = function(){ - _done.call(this); - - submit({ - fail: this.failureCount, - error: this.errorCount, - total: this.totalCount - }); - }; - - window.TestSwarm.serialize = function(){ - return "<pre>" + this.log.join("\n") + "</pre>"; - }; - - // Selenium Core - // http://seleniumhq.org/projects/core/ - } else if ( typeof SeleniumTestResult !== "undefined" && typeof LOG !== "undefined" ) { - // Completely overwrite the postback - SeleniumTestResult.prototype.post = function(){ - submit({ - fail: this.metrics.numCommandFailures, - error: this.metrics.numCommandErrors, - total: this.metrics.numCommandPasses + this.metrics.numCommandFailures + this.metrics.numCommandErrors - }); - }; - - window.TestSwarm.serialize = function(){ - var results = []; - while ( LOG.pendingMessages.length ) { - var msg = LOG.pendingMessages.shift(); - results.push( msg.type + ": " + msg.msg ); - } - - return "<pre>" + results.join("\n") + "</pre>"; - }; - - // Dojo Objective Harness - // http://docs.dojocampus.org/quickstart/doh - } else if ( typeof doh !== "undefined" && doh._report ) { - var _report = doh._report; - doh._report = function(){ - _report.apply(this, arguments); - - submit({ - fail: doh._failureCount, - error: doh._errorCount, - total: doh._testCount - }); - }; - - window.TestSwarm.serialize = function(){ - return "<pre>" + document.getElementById("logBody").innerHTML + "</pre>"; - }; - // Screw.Unit - // git://github.com/nathansobo/screw-unit.git - } else if ( typeof Screw !== "undefined" && typeof jQuery !== 'undefined' && Screw && Screw.Unit ) { - $(Screw).bind("after", function() { - var passed = $('.passed').length; - var failed = $('.failed').length; - submit({ - fail: failed, - error: 0, - total: failed + passed - }); - }); - - $(Screw).bind("loaded", function() { - $('.it') - .bind("passed", window.TestSwarm.heartbeat) - .bind("failed", window.TestSwarm.heartbeat); - window.TestSwarm.heartbeat(); - }); - - window.TestSwarm.serialize = function(){ - return trimSerialize(); - }; - } - - function trimSerialize(doc) { - doc = doc || document; - - var scripts = doc.getElementsByTagName("script"); - while ( scripts.length ) { - remove( scripts[0] ); - } - - var root = window.location.href.replace(/(https?:\/\/.*?)\/.*/, "$1"); - var cur = window.location.href.replace(/[^\/]*$/, ""); - - var links = doc.getElementsByTagName("link"); - for ( var i = 0; i < links.length; i++ ) { - var href = links[i].href; - if ( href.indexOf("/") === 0 ) { - href = root + href; - } else if ( !/^https?:\/\//.test( href ) ) { - href = cur + href; - } - links[i].href = href; - } - - return ("<html>" + doc.documentElement.innerHTML + "</html>") - .replace(/\s+/g, " "); - } - - function remove(elem){ - if ( typeof elem === "string" ) { - elem = document.getElementById( elem ); - } - - if ( elem ) { - elem.parentNode.removeChild( elem ); - } - } - - function submit(params){ - if ( curHeartbeat ) { - clearTimeout( curHeartbeat ); - } - - var paramItems = (url.split("?")[1] || "").split("&"); - - for ( var i = 0; i < paramItems.length; i++ ) { - if ( paramItems[i] ) { - var parts = paramItems[i].split("="); - if ( !params[ parts[0] ] ) { - params[ parts[0] ] = parts[1]; - } - } - } - - if ( !params.state ) { - params.state = "saverun"; - } - - if ( !params.results ) { - params.results = window.TestSwarm.serialize(); - } - - if ( doPost ) { - // Build Query String - var query = ""; - - for ( var i in params ) { - query += ( query ? "&" : "" ) + i + "=" + - encodeURIComponent(params[i]); - } - - if ( DEBUG ) { - alert( query ); - } else { - window.top.postMessage( query, "*" ); - } - - } else { - var form = document.createElement("form"); - form.action = url; - form.method = "POST"; - - for ( var i in params ) { - var input = document.createElement("input"); - input.type = "hidden"; - input.name = i; - input.value = params[i]; - form.appendChild( input ); - } - - if ( DEBUG ) { - alert( form.innerHTML ); - } else { - - // Watch for the result submission timing out - setTimeout(function(){ - submit( params ); - }, submitTimeout * 1000); - - document.body.appendChild( form ); - form.submit(); - } - } - } - -})(); diff --git a/tests/qunit/index.html b/tests/qunit/index.html deleted file mode 100644 index ef7ff8de..00000000 --- a/tests/qunit/index.html +++ /dev/null @@ -1,139 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>MediaWiki JavaScript Test Suite</title> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> - <!-- MediaWiki Modules --> - - <!-- MW: startup --> - <script> - function startUp(){ - mw.config = new mw.Map( false ); - - /** - * Simulate an average mw.config context - */ - /* StartUp module */ - mw.config.set({"wgLoadScript": "/mw/trunk/phase3/load.php", "debug": true, "skin": "vector", "stylepath": "/mw/trunk/phase3/skins", "wgUrlProtocols": "http\\:\\/\\/|https\\:\\/\\/|ftp\\:\\/\\/|irc\\:\\/\\/|ircs\\:\\/\\/|gopher\\:\\/\\/|telnet\\:\\/\\/|nntp\\:\\/\\/|worldwind\\:\\/\\/|mailto\\:|news\\:|svn\\:\\/\\/|git\\:\\/\\/|mms\\:\\/\\/|\\/\\/", "wgArticlePath": "/mw/trunk/phase3/index.php/$1", "wgScriptPath": "/mw/trunk/phase3", "wgScriptExtension": ".php", "wgScript": "/mw/trunk/phase3/index.php", "wgVariantArticlePath": false, "wgActionPaths": [], "wgServer": "http://localhost", "wgUserLanguage": "en", "wgContentLanguage": "en", "wgVersion": "1.19alpha", "wgEnableAPI": true, "wgEnableWriteAPI": true, "wgDefaultDateFormat": "dmy", "wgMonthNames": ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "wgMonthNamesShort": ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "wgMainPageTitle": "Main Page", "wgFormattedNamespaces": {"-2": "Media", "-1": "Special", "0": "", "1": "Talk", "2": "User", "3": "User talk", "4": "Testopedia", "5": "Testopedia talk", "6": "File", "7": "File talk", "8": "MediaWiki", "9": "MediaWiki talk", "10": "Template", "11": "Template talk", "12": "Help", "13": "Help talk", "14": "Category", "15": "Category talk"}, "wgNamespaceIds": {"media": -2, "special": -1, "": 0, "talk": 1, "user": 2, "user_talk": 3, "testopedia": 4, "testopedia_talk": 5, "file": 6, "file_talk": 7, "mediawiki": 8, "mediawiki_talk": 9, "template": 10, "template_talk": 11, "help": 12, "help_talk": 13, "category": 14, "category_talk": 15, "image": 6, "image_talk": 7, "project": 4, "project_talk": 5}, "wgSiteName": "Testopedia", "wgFileExtensions": ["png", "gif", "jpg", "jpeg"], "wgDBname": "mediawiki", "wgFileCanRotate": true, "wgAvailableSkins": {"chick": "Chick", "cologneblue": "CologneBlue", "modern": "Modern", "monobook": "MonoBook", "myskin": "MySkin", "nostalgia": "Nostalgia", "simple": "Simple", "standard": "Standard", "vector": "Vector"}, "wgExtensionAssetsPath": "/mw/trunk/phase3/extensions", "wgCookiePrefix": "mediawiki", "wgResourceLoaderMaxQueryLength": -1, "wgCaseSensitiveNamespaces": []}); - - /* WikiPage specific */ - mw.config.set({"wgCanonicalNamespace": "", "wgCanonicalSpecialPageName": false, "wgNamespaceNumber": 0, "wgPageName": "Sandbox", "wgTitle": "Sandbox", "wgCurRevisionId": 486, "wgArticleId": 84, "wgIsArticle": true, "wgAction": "view", "wgUserName": null, "wgUserGroups": ["*"], "wgCategories": [], "wgBreakFrames": false, "wgPageContentLanguage": "en", "wgSeparatorTransformTable": ["", ""], "wgDigitTransformTable": ["", ""], "wgRestrictionEdit": [], "wgRestrictionMove": [], "wgRedirectedFrom": "Sandbox"}); - - /** - * Fix wgScriptPath and the like to the real thing, - * instead of fake ones (for access to /tests/qunit/data/) - */ - - // Regular expression to extract the path for the QUnit tests - // Takes into account that tests could be run from a file:// URL - // by excluding the 'index.html' part from the URL - var rePath = /(?:[^#\?](?!index.html))*\/?/; - - // Extract path to /tests/qunit/ - var qunitTestsPath = rePath.exec( location.pathname )[0]; - - // Traverse up to script path - var pathParts = qunitTestsPath.split( '/' ); - pathParts.pop(); pathParts.pop(); pathParts.pop(); - var scriptPath = pathParts.join( '/' ); - - mw.config.set({ - "wgServer": location.protocol + '//' + location.host, - "wgScriptPath": scriptPath, - "wgLoadScript": scriptPath + '/load.php', - "stylepath": scriptPath + '/skins', - "wgArticlePath": scriptPath + '/index.php/$1', - "wgScript": scriptPath + '/index.php', - "wgExtensionAssetsPath": scriptPath + '/extensions' - }); - } - </script> - - <!-- MW: jquery|mediawiki --> - <script src="../../resources/jquery/jquery.js"></script> - <script src="../../resources/mediawiki/mediawiki.js"></script> - - <!-- MW: mediawiki.page.startup --> - <script src="../../resources/jquery/jquery.client.js"></script> - <script src="../../resources/mediawiki/mediawiki.util.js"></script> - <script src="../../resources/mediawiki.page/mediawiki.page.startup.js"></script> - - <!-- MW: mediawiki.user|mediawiki.page.ready --> - <script src="../../resources/jquery/jquery.cookie.js"></script> - <script src="../../resources/mediawiki/mediawiki.user.js"></script> - - <script src="../../resources/jquery/jquery.messageBox.js"></script> - <script src="../../resources/jquery/jquery.mwExtension.js"></script> - - <script src="../../resources/jquery/jquery.checkboxShiftClick.js"></script> - <script src="../../resources/jquery/jquery.makeCollapsible.js"></script> - <script src="../../resources/jquery/jquery.placeholder.js"></script> - <script src="../../resources/mediawiki.page/mediawiki.page.ready.js"></script> - - <!-- MW: user.options --> - <script> - mw.user.options.set({"skin": "vector"}); - </script> - - <!-- MW: Non-default modules --> - <script src="../../resources/jquery/jquery.autoEllipsis.js"></script> - <script src="../../resources/jquery/jquery.byteLength.js"></script> - <script src="../../resources/jquery/jquery.byteLimit.js"></script> - <script src="../../resources/jquery/jquery.colorUtil.js"></script> - <script src="../../resources/jquery/jquery.delayedBind.js"></script> - <script src="../../resources/jquery/jquery.getAttrs.js"></script> - <script src="../../resources/jquery/jquery.highlightText.js"></script> - <script src="../../resources/jquery/jquery.localize.js"></script> - <script src="../../resources/jquery/jquery.tabIndex.js"></script> - <script src="../../resources/jquery/jquery.tablesorter.js"></script> - <script src="../../resources/jquery/jquery.textSelection.js"></script> - <script src="../../resources/mediawiki/mediawiki.Title.js"></script> - <script src="../../resources/mediawiki.language/mediawiki.language.js"></script> - <script src="../../resources/mediawiki/mediawiki.jqueryMsg.js"></script> - <script src="../../resources/mediawiki.special/mediawiki.special.js"></script> - <script src="../../resources/mediawiki.special/mediawiki.special.recentchanges.js"></script> - - <!-- QUnit: Load framework --> - <link rel="stylesheet" href="../../resources/jquery/jquery.qunit.css" /> - <script src="../../resources/jquery/jquery.qunit.js"></script> - <script src="../../resources/jquery/jquery.qunit.completenessTest.js"></script> - <script src="data/testrunner.js"></script> - - <!-- QUnit: Load test suites (maintain the same order as above please) --> - <script src="suites/resources/mediawiki/mediawiki.jscompat.test.js"></script> - <script src="suites/resources/mediawiki/mediawiki.test.js"></script> - <script src="suites/resources/mediawiki/mediawiki.user.test.js"></script> - - <script src="suites/resources/jquery/jquery.client.test.js"></script> - <script src="suites/resources/jquery/jquery.mwExtension.test.js"></script> - <script src="suites/resources/mediawiki/mediawiki.util.test.js"></script> - - <script src="suites/resources/jquery/jquery.autoEllipsis.test.js"></script> - <script src="suites/resources/jquery/jquery.byteLength.test.js"></script> - <script src="suites/resources/jquery/jquery.byteLimit.test.js"></script> - <script src="suites/resources/jquery/jquery.colorUtil.test.js"></script> - <script src="suites/resources/jquery/jquery.delayedBind.test.js"></script> - <script src="suites/resources/jquery/jquery.getAttrs.test.js"></script> - <script src="suites/resources/jquery/jquery.highlightText.test.js"></script> - <script src="suites/resources/jquery/jquery.localize.test.js"></script> - <script src="suites/resources/jquery/jquery.tabIndex.test.js"></script> - <script src="suites/resources/jquery/jquery.tablesorter.test.js" charset="UTF-8"></script> - <script src="suites/resources/jquery/jquery.textSelection.test.js" charset="UTF-8"></script> - <script src="suites/resources/mediawiki/mediawiki.Title.test.js"></script> - <script src="suites/resources/mediawiki/mediawiki.jqueryMsg.test.js"></script> - <script src="suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js"></script> -</head> -<body> - <h1 id="qunit-header">MediaWiki JavaScript Test Suite</h1> - <h2 id="qunit-banner"></h2> - <div id="qunit-testrunner-toolbar"> - <p><a href="http://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing">See testing documentation on mediawiki.org</a></p> - </div> - <h2 id="qunit-userAgent"></h2> - <ol id="qunit-tests"></ol> - <div id="qunit-fixture"></div> - -<!-- Scripts inserting stuff here shall remove it themselfs! --> -<div id="content"></div> -</body> -</html> diff --git a/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js index 6e371384..0dee2ef0 100644 --- a/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js @@ -1,9 +1,6 @@ -module( 'jquery.autoEllipsis', QUnit.newMwEnvironment() ); +( function ( mw, $ ) { -test( '-- Initial check', function() { - expect(1); - ok( $.fn.autoEllipsis, 'jQuery.fn.autoEllipsis defined' ); -}); +QUnit.module( 'jquery.autoEllipsis', QUnit.newMwEnvironment() ); function createWrappedDiv( text, width ) { var $wrapper = $( '<div>' ).css( 'width', width ); @@ -14,15 +11,13 @@ function createWrappedDiv( text, width ) { function findDivergenceIndex( a, b ) { var i = 0; - while ( i < a.length && i < b.length && a[i] == b[i] ) { + while ( i < a.length && i < b.length && a[i] === b[i] ) { i++; } return i; } -test( 'Position right', function() { - expect(4); - +QUnit.test( 'Position right', 4, function ( assert ) { // We need this thing to be visible, so append it to the DOM var origText = 'This is a really long random string and there is no way it fits in 100 pixels.'; var $wrapper = createWrappedDiv( origText, '100px' ); @@ -31,25 +26,27 @@ test( 'Position right', function() { // Verify that, and only one, span element was created var $span = $wrapper.find( '> span' ); - strictEqual( $span.length, 1, 'autoEllipsis wrapped the contents in a span element' ); + assert.strictEqual( $span.length, 1, 'autoEllipsis wrapped the contents in a span element' ); // Check that the text fits by turning on word wrapping $span.css( 'whiteSpace', 'nowrap' ); - ltOrEq( $span.width(), $span.parent().width(), "Text fits (making the span 'white-space:nowrap' does not make it wider than its parent)" ); + assert.ltOrEq( $span.width(), $span.parent().width(), "Text fits (making the span 'white-space:nowrap' does not make it wider than its parent)" ); // Add two characters using scary black magic var spanText = $span.text(); var d = findDivergenceIndex( origText, spanText ); var spanTextNew = spanText.substr( 0, d ) + origText[d] + origText[d] + '...'; - gt( spanTextNew.length, spanText.length, 'Verify that the new span-length is indeed greater' ); + assert.gt( spanTextNew.length, spanText.length, 'Verify that the new span-length is indeed greater' ); // Put this text in the span and verify it doesn't fit $span.text( spanTextNew ); // In IE6 width works like min-width, allow IE6's width to be "equal to" if ( $.browser.msie && Number( $.browser.version ) === 6 ) { - gtOrEq( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more) - IE6: Maybe equal to as well due to width behaving like min-width in IE6' ); + assert.gtOrEq( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more) - IE6: Maybe equal to as well due to width behaving like min-width in IE6' ); } else { - gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' ); + assert.gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' ); } }); + +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js index 15fac691..a6ddfca6 100644 --- a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js @@ -1,32 +1,23 @@ -module( 'jquery.byteLength', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(1); - ok( $.byteLength, 'jQuery.byteLength defined' ); -} ); - -test( 'Simple text', function() { - expect(5); +QUnit.module( 'jquery.byteLength', QUnit.newMwEnvironment() ); +QUnit.test( 'Simple text', 5, function ( assert ) { var azLc = 'abcdefghijklmnopqrstuvwxyz', azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', num = '0123456789', x = '*', space = ' '; - equal( $.byteLength( azLc ), 26, 'Lowercase a-z' ); - equal( $.byteLength( azUc ), 26, 'Uppercase A-Z' ); - equal( $.byteLength( num ), 10, 'Numbers 0-9' ); - equal( $.byteLength( x ), 1, 'An asterisk' ); - equal( $.byteLength( space ), 3, '3 spaces' ); + assert.equal( $.byteLength( azLc ), 26, 'Lowercase a-z' ); + assert.equal( $.byteLength( azUc ), 26, 'Uppercase A-Z' ); + assert.equal( $.byteLength( num ), 10, 'Numbers 0-9' ); + assert.equal( $.byteLength( x ), 1, 'An asterisk' ); + assert.equal( $.byteLength( space ), 3, '3 spaces' ); } ); -test( 'Special text', window.foo = function() { - expect(5); - +QUnit.test( 'Special text', 5, function ( assert ) { // http://en.wikipedia.org/wiki/UTF-8 - var U_0024 = '\u0024', + var U_0024 = '$', U_00A2 = '\u00A2', U_20AC = '\u20AC', U_024B62 = '\u024B62', @@ -34,9 +25,9 @@ test( 'Special text', window.foo = function() { // according to http://www.fileformat.info/info/unicode/char/24B62/index.htm U_024B62_alt = '\uD852\uDF62'; - strictEqual( $.byteLength( U_0024 ), 1, 'U+0024: 1 byte. \u0024 (dollar sign)' ); - strictEqual( $.byteLength( U_00A2 ), 2, 'U+00A2: 2 bytes. \u00A2 (cent sign)' ); - strictEqual( $.byteLength( U_20AC ), 3, 'U+20AC: 3 bytes. \u20AC (euro sign)' ); - strictEqual( $.byteLength( U_024B62 ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character)' ); - strictEqual( $.byteLength( U_024B62_alt ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character) - alternative method' ); + assert.strictEqual( $.byteLength( U_0024 ), 1, 'U+0024: 1 byte. $ (dollar sign)' ); + assert.strictEqual( $.byteLength( U_00A2 ), 2, 'U+00A2: 2 bytes. \u00A2 (cent sign)' ); + assert.strictEqual( $.byteLength( U_20AC ), 3, 'U+20AC: 3 bytes. \u20AC (euro sign)' ); + assert.strictEqual( $.byteLength( U_024B62 ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character)' ); + assert.strictEqual( $.byteLength( U_024B62_alt ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character) - alternative method' ); } ); diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js index 3346c2d5..4f86eb96 100644 --- a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js @@ -1,187 +1,234 @@ -( function () { - -module( 'jquery.byteLimit', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(1); - ok( $.fn.byteLimit, 'jQuery.fn.byteLimit defined' ); -} ); - -// Basic sendkey-implementation -$.addChars = function( $input, charstr ) { - var len = charstr.length; - for ( var i = 0; i < len; i++ ) { - // Keep track of the previous value - var prevVal = $input.val(); - - // Get the key code - var code = charstr.charCodeAt(i); - - // Trigger event and undo if prevented - var event = new jQuery.Event( 'keypress', { keyCode: code, which: code, charCode: code } ); - $input.trigger( event ); - if ( !event.isDefaultPrevented() ) { - $input.val( prevVal + charstr.charAt(i) ); - } - } -}; - -/** - * Test factory for $.fn.byteLimit - * - * @param $input {jQuery} jQuery object in an input element - * @param hasLimit {Boolean} Wether a limit should apply at all - * @param limit {Number} Limit (if used) otherwise undefined - * The limit should be less than 20 (the sample data's length) - */ -var byteLimitTest = function( options ) { - var opt = $.extend({ - description: '', - $input: null, - sample: '', - hasLimit: false, - expected: '', - limit: null - }, options); - - test( opt.description, function() { - - opt.$input.appendTo( '#qunit-fixture' ); - - // Simulate pressing keys for each of the sample characters - $.addChars( opt.$input, opt.sample ); - var rawVal = opt.$input.val(), - fn = opt.$input.data( 'byteLimit-callback' ), - newVal = $.isFunction( fn ) ? fn( rawVal ) : rawVal; - - if ( opt.hasLimit ) { - expect(3); - - ltOrEq( $.byteLength( newVal ), opt.limit, 'Prevent keypresses after byteLimit was reached, length never exceeded the limit' ); - equal( $.byteLength( rawVal ), $.byteLength( opt.expected ), 'Not preventing keypresses too early, length has reached the expected length' ); - equal( rawVal, opt.expected, 'New value matches the expected string' ); +( function ( $, mw ) { + var simpleSample, U_20AC, mbSample; - } else { - expect(2); - equal( newVal, opt.expected, 'New value matches the expected string' ); - equal( $.byteLength( newVal ), $.byteLength( opt.expected ), 'Unlimited scenarios are not affected, expected length reached' ); - } - } ); -}; + QUnit.module( 'jquery.byteLimit', QUnit.newMwEnvironment() ); -var // Simple sample (20 chars, 20 bytes) - simpleSample = '12345678901234567890', + simpleSample = '12345678901234567890'; // 3 bytes (euro-symbol) - U_20AC = '\u20AC', + U_20AC = '\u20AC'; // Multi-byte sample (22 chars, 26 bytes) mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; -byteLimitTest({ - description: 'Plain text input', - $input: $( '<input>' ) - .attr( 'type', 'text' ), - sample: simpleSample, - hasLimit: false, - expected: simpleSample -}); - -byteLimitTest({ - description: 'Limit using the maxlength attribute', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .prop( 'maxLength', '10' ) - .byteLimit(), - sample: simpleSample, - hasLimit: true, - limit: 10, - expected: '1234567890' -}); - -byteLimitTest({ - description: 'Limit using a custom value', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .byteLimit( 10 ), - sample: simpleSample, - hasLimit: true, - limit: 10, - expected: '1234567890' -}); - -byteLimitTest({ - description: 'Limit using a custom value, overriding maxlength attribute', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .prop( 'maxLength', '10' ) - .byteLimit( 15 ), - sample: simpleSample, - hasLimit: true, - limit: 15, - expected: '123456789012345' -}); - -byteLimitTest({ - description: 'Limit using a custom value (multibyte)', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .byteLimit( 14 ), - sample: mbSample, - hasLimit: true, - limit: 14, - expected: '1234567890' + U_20AC + '1' -}); - -byteLimitTest({ - description: 'Limit using a custom value (multibyte) overlapping a byte', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .byteLimit( 12 ), - sample: mbSample, - hasLimit: true, - limit: 12, - expected: '1234567890' + '12' -}); - -byteLimitTest({ - description: 'Pass the limit and a callback as input filter', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .byteLimit( 6, function( val ) { - // Invalid title - if ( val == '' ) { - return ''; - } + // Basic sendkey-implementation + function addChars( $input, charstr ) { + var c, len; + for ( c = 0, len = charstr.length; c < len; c += 1 ) { + $input + .val( function ( i, val ) { + // Add character to the value + return val + charstr.charAt( c ); + } ) + .trigger( 'change' ); + } + } - // Return without namespace prefix - return new mw.Title( '' + val ).getMain(); - } ), - sample: 'User:Sample', - hasLimit: true, - limit: 6, // 'Sample' length - expected: 'User:Sample' -}); - -byteLimitTest({ - description: 'Limit using the maxlength attribute and pass a callback as input filter', - $input: $( '<input>' ) - .attr( 'type', 'text' ) - .prop( 'maxLength', '6' ) - .byteLimit( function( val ) { - // Invalid title - if ( val === '' ) { - return ''; + /** + * Test factory for $.fn.byteLimit + * + * @param $input {jQuery} jQuery object in an input element + * @param hasLimit {Boolean} Wether a limit should apply at all + * @param limit {Number} Limit (if used) otherwise undefined + * The limit should be less than 20 (the sample data's length) + */ + function byteLimitTest( options ) { + var opt = $.extend({ + description: '', + $input: null, + sample: '', + hasLimit: false, + expected: '', + limit: null + }, options); + + QUnit.asyncTest( opt.description, opt.hasLimit ? 3 : 2, function ( assert ) { + setTimeout( function () { + var rawVal, fn, effectiveVal; + + opt.$input.appendTo( '#qunit-fixture' ); + + // Simulate pressing keys for each of the sample characters + addChars( opt.$input, opt.sample ); + + rawVal = opt.$input.val(); + fn = opt.$input.data( 'byteLimit.callback' ); + effectiveVal = fn ? fn( rawVal ) : rawVal; + + if ( opt.hasLimit ) { + assert.ltOrEq( + $.byteLength( effectiveVal ), + opt.limit, + 'Prevent keypresses after byteLimit was reached, length never exceeded the limit' + ); + assert.equal( + $.byteLength( rawVal ), + $.byteLength( opt.expected ), + 'Not preventing keypresses too early, length has reached the expected length' + ); + assert.equal( rawVal, opt.expected, 'New value matches the expected string' ); + + } else { + assert.equal( + $.byteLength( effectiveVal ), + $.byteLength( opt.expected ), + 'Unlimited scenarios are not affected, expected length reached' + ); + assert.equal( rawVal, opt.expected, 'New value matches the expected string' ); } + QUnit.start(); + }, 10 ); + } ); + } - // Return without namespace prefix - return new mw.Title( '' + val ).getMain(); - } ), - sample: 'User:Sample', - hasLimit: true, - limit: 6, // 'Sample' length - expected: 'User:Sample' -}); - -}() );
\ No newline at end of file + byteLimitTest({ + description: 'Plain text input', + $input: $( '<input type="text"/>' ), + sample: simpleSample, + hasLimit: false, + expected: simpleSample + }); + + byteLimitTest({ + description: 'Plain text input. Calling byteLimit with no parameters and no maxlength attribute (bug 36310)', + $input: $( '<input type="text"/>' ) + .byteLimit(), + sample: simpleSample, + hasLimit: false, + expected: simpleSample + }); + + byteLimitTest({ + description: 'Limit using the maxlength attribute', + $input: $( '<input type="text"/>' ) + .attr( 'maxlength', '10' ) + .byteLimit(), + sample: simpleSample, + hasLimit: true, + limit: 10, + expected: '1234567890' + }); + + byteLimitTest({ + description: 'Limit using a custom value', + $input: $( '<input type="text"/>' ) + .byteLimit( 10 ), + sample: simpleSample, + hasLimit: true, + limit: 10, + expected: '1234567890' + }); + + byteLimitTest({ + description: 'Limit using a custom value, overriding maxlength attribute', + $input: $( '<input type="text"/>' ) + .attr( 'maxlength', '10' ) + .byteLimit( 15 ), + sample: simpleSample, + hasLimit: true, + limit: 15, + expected: '123456789012345' + }); + + byteLimitTest({ + description: 'Limit using a custom value (multibyte)', + $input: $( '<input type="text"/>' ) + .byteLimit( 14 ), + sample: mbSample, + hasLimit: true, + limit: 14, + expected: '1234567890' + U_20AC + '1' + }); + + byteLimitTest({ + description: 'Limit using a custom value (multibyte) overlapping a byte', + $input: $( '<input type="text"/>' ) + .byteLimit( 12 ), + sample: mbSample, + hasLimit: true, + limit: 12, + expected: '1234567890' + '12' + }); + + byteLimitTest({ + description: 'Pass the limit and a callback as input filter', + $input: $( '<input type="text"/>' ) + .byteLimit( 6, function ( val ) { + // Invalid title + if ( val === '' ) { + return ''; + } + + // Return without namespace prefix + return new mw.Title( String( val ) ).getMain(); + } ), + sample: 'User:Sample', + hasLimit: true, + limit: 6, // 'Sample' length + expected: 'User:Sample' + }); + + byteLimitTest({ + description: 'Limit using the maxlength attribute and pass a callback as input filter', + $input: $( '<input type="text"/>' ) + .attr( 'maxlength', '6' ) + .byteLimit( function ( val ) { + // Invalid title + if ( val === '' ) { + return ''; + } + + // Return without namespace prefix + return new mw.Title( String( val ) ).getMain(); + } ), + sample: 'User:Sample', + hasLimit: true, + limit: 6, // 'Sample' length + expected: 'User:Sample' + }); + + QUnit.test( 'Confirm properties and attributes set', 4, function ( assert ) { + var $el, $elA, $elB; + + $el = $( '<input type="text"/>' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit(); + + assert.strictEqual( $el.attr( 'maxlength' ), '7', 'maxlength attribute unchanged for simple limit' ); + + $el = $( '<input type="text"/>' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ); + + assert.strictEqual( $el.attr( 'maxlength' ), '12', 'maxlength attribute updated for custom limit' ); + + $el = $( '<input type="text"/>' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12, function ( val ) { + return val; + } ); + + assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' ); + + $elA = $( '<input type="text"/>' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ); + + $elB = $( '<input type="text"/>' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'maxlength', '12' ) + .appendTo( '#qunit-fixture' ); + + $el = $( '.mw-test-byteLimit-foo' ); + + assert.strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' ); + + $el.byteLimit(); + }); + +}( jQuery, mediaWiki ) ); diff --git a/tests/qunit/suites/resources/jquery/jquery.client.test.js b/tests/qunit/suites/resources/jquery/jquery.client.test.js index 7be41971..bf62b39a 100644 --- a/tests/qunit/suites/resources/jquery/jquery.client.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.client.test.js @@ -1,14 +1,9 @@ -module( 'jquery.client', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(1); - ok( jQuery.client, 'jQuery.client defined' ); -}); +QUnit.module( 'jquery.client', QUnit.newMwEnvironment() ); /** Number of user-agent defined */ var uacount = 0; -var uas = (function() { +var uas = (function () { // Object keyed by userAgent. Value is an array (human-readable name, client-profile object, navigator.platform value) // Info based on results from http://toolserver.org/~krinkle/testswarm/job/174/ @@ -205,42 +200,59 @@ var uas = (function() { ltr: true, rtl: true } + }, + // Bug #34924 + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.34 (KHTML, like Gecko) rekonq Safari/534.34': { + title: 'Rekonq', + platform: 'Linux i686', + profile: { + "name": "rekonq", + "layout": "webkit", + "layoutVersion": 534, + "platform": "linux", + "version": "534.34", + "versionBase": "534", + "versionNumber": 534.34 + }, + wikiEditor: { + ltr: true, + rtl: true + } } }; - $.each( uas, function() { uacount++ }); + $.each( uas, function () { + uacount++; + }); return uas; -})(); - -test( 'profile userAgent support', function() { - expect(uacount); +}()); +QUnit.test( 'profile userAgent support', uacount, function ( assert ) { // Generate a client profile object and compare recursively var uaTest = function( rawUserAgent, data ) { var ret = $.client.profile( { userAgent: rawUserAgent, platform: data.platform } ); - deepEqual( ret, data.profile, 'Client profile support check for ' + data.title + ' (' + data.platform + '): ' + rawUserAgent ); + assert.deepEqual( ret, data.profile, 'Client profile support check for ' + data.title + ' (' + data.platform + '): ' + rawUserAgent ); }; // Loop through and run tests $.each( uas, uaTest ); } ); -test( 'profile return validation for current user agent', function() { - expect(7); +QUnit.test( 'profile return validation for current user agent', 7, function ( assert ) { var p = $.client.profile(); - var unknownOrType = function( val, type, summary ) { - return ok( typeof val === type || val === 'unknown', summary ); - }; + function unknownOrType( val, type, summary ) { + assert.ok( typeof val === type || val === 'unknown', summary ); + } - equal( typeof p, 'object', 'profile returns an object' ); + assert.equal( typeof p, 'object', 'profile returns an object' ); unknownOrType( p.layout, 'string', 'p.layout is a string (or "unknown")' ); unknownOrType( p.layoutVersion, 'number', 'p.layoutVersion is a number (or "unknown")' ); unknownOrType( p.platform, 'string', 'p.platform is a string (or "unknown")' ); unknownOrType( p.version, 'string', 'p.version is a string (or "unknown")' ); unknownOrType( p.versionBase, 'string', 'p.versionBase is a string (or "unknown")' ); - equal( typeof p.versionNumber, 'number', 'p.versionNumber is a number' ); + assert.equal( typeof p.versionNumber, 'number', 'p.versionNumber is a number' ); }); // Example from WikiEditor @@ -271,20 +283,16 @@ var testMap = { } }; -test( 'test', function() { - expect(1); - +QUnit.test( 'test', 1, function ( assert ) { // .test() uses eval, make sure no exceptions are thrown // then do a basic return value type check var testMatch = $.client.test( testMap ); - equal( typeof testMatch, 'boolean', 'test returns a boolean value' ); + assert.equal( typeof testMatch, 'boolean', 'test returns a boolean value' ); }); -test( 'User-agent matches against WikiEditor\'s compatibility map', function() { - expect( uacount * 2 ); // double since we test both LTR and RTL - +QUnit.test( 'User-agent matches against WikiEditor\'s compatibility map', uacount * 2, function ( assert ) { var $body = $( 'body' ), bodyClasses = $body.attr( 'class' ); @@ -299,7 +307,7 @@ test( 'User-agent matches against WikiEditor\'s compatibility map', function() { var testMatch = $.client.test( testMap, profile ); $body.removeClass( dir ); - equal( testMatch, data.wikiEditor[dir], 'testing comparison based on ' + dir + ', ' + agent ); + assert.equal( testMatch, data.wikiEditor[dir], 'testing comparison based on ' + dir + ', ' + agent ); }); }); diff --git a/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js b/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js index 655ee564..7b37f5a0 100644 --- a/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js @@ -1,40 +1,31 @@ -module( 'jquery.colorUtil', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(1); - ok( $.colorUtil, '$.colorUtil defined' ); -}); - -test( 'getRGB', function() { - expect(18); - - strictEqual( $.colorUtil.getRGB(), undefined, 'No arguments' ); - strictEqual( $.colorUtil.getRGB( '' ), undefined, 'Empty string' ); - deepEqual( $.colorUtil.getRGB( [0, 100, 255] ), [0, 100, 255], 'Parse array of rgb values' ); - deepEqual( $.colorUtil.getRGB( 'rgb(0,100,255)' ), [0, 100, 255], 'Parse simple rgb string' ); - deepEqual( $.colorUtil.getRGB( 'rgb(0, 100, 255)' ), [0, 100, 255], 'Parse simple rgb string with spaces' ); - deepEqual( $.colorUtil.getRGB( 'rgb(0%,20%,40%)' ), [0, 51, 102], 'Parse rgb string with percentages' ); - deepEqual( $.colorUtil.getRGB( 'rgb(0%, 20%, 40%)' ), [0, 51, 102], 'Parse rgb string with percentages and spaces' ); - deepEqual( $.colorUtil.getRGB( '#f2ddee' ), [242, 221, 238], 'Hex string: 6 char lowercase' ); - deepEqual( $.colorUtil.getRGB( '#f2DDEE' ), [242, 221, 238], 'Hex string: 6 char uppercase' ); - deepEqual( $.colorUtil.getRGB( '#f2DdEe' ), [242, 221, 238], 'Hex string: 6 char mixed' ); - deepEqual( $.colorUtil.getRGB( '#eee' ), [238, 238, 238], 'Hex string: 3 char lowercase' ); - deepEqual( $.colorUtil.getRGB( '#EEE' ), [238, 238, 238], 'Hex string: 3 char uppercase' ); - deepEqual( $.colorUtil.getRGB( '#eEe' ), [238, 238, 238], 'Hex string: 3 char mixed' ); - deepEqual( $.colorUtil.getRGB( 'rgba(0, 0, 0, 0)' ), [255, 255, 255], 'Zero rgba for Safari 3; Transparent (whitespace)' ); +QUnit.module( 'jquery.colorUtil', QUnit.newMwEnvironment() ); + +QUnit.test( 'getRGB', 18, function ( assert ) { + assert.strictEqual( $.colorUtil.getRGB(), undefined, 'No arguments' ); + assert.strictEqual( $.colorUtil.getRGB( '' ), undefined, 'Empty string' ); + assert.deepEqual( $.colorUtil.getRGB( [0, 100, 255] ), [0, 100, 255], 'Parse array of rgb values' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0,100,255)' ), [0, 100, 255], 'Parse simple rgb string' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0, 100, 255)' ), [0, 100, 255], 'Parse simple rgb string with spaces' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0%,20%,40%)' ), [0, 51, 102], 'Parse rgb string with percentages' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0%, 20%, 40%)' ), [0, 51, 102], 'Parse rgb string with percentages and spaces' ); + assert.deepEqual( $.colorUtil.getRGB( '#f2ddee' ), [242, 221, 238], 'Hex string: 6 char lowercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#f2DDEE' ), [242, 221, 238], 'Hex string: 6 char uppercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#f2DdEe' ), [242, 221, 238], 'Hex string: 6 char mixed' ); + assert.deepEqual( $.colorUtil.getRGB( '#eee' ), [238, 238, 238], 'Hex string: 3 char lowercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#EEE' ), [238, 238, 238], 'Hex string: 3 char uppercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#eEe' ), [238, 238, 238], 'Hex string: 3 char mixed' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgba(0, 0, 0, 0)' ), [255, 255, 255], 'Zero rgba for Safari 3; Transparent (whitespace)' ); // Perhaps this is a bug in colorUtil, but it is the current behaviour so, let's keep // track of it, so we will know in case it would ever change. - strictEqual( $.colorUtil.getRGB( 'rgba(0,0,0,0)' ), undefined, 'Zero rgba without whitespace' ); + assert.strictEqual( $.colorUtil.getRGB( 'rgba(0,0,0,0)' ), undefined, 'Zero rgba without whitespace' ); - deepEqual( $.colorUtil.getRGB( 'lightGreen' ), [144, 238, 144], 'Color names (lightGreen)' ); - deepEqual( $.colorUtil.getRGB( 'transparent' ), [255, 255, 255], 'Color names (transparent)' ); - strictEqual( $.colorUtil.getRGB( 'mediaWiki' ), undefined, 'Inexisting color name' ); + assert.deepEqual( $.colorUtil.getRGB( 'lightGreen' ), [144, 238, 144], 'Color names (lightGreen)' ); + assert.deepEqual( $.colorUtil.getRGB( 'transparent' ), [255, 255, 255], 'Color names (transparent)' ); + assert.strictEqual( $.colorUtil.getRGB( 'mediaWiki' ), undefined, 'Inexisting color name' ); }); -test( 'rgbToHsl', function() { - expect(1); - +QUnit.test( 'rgbToHsl', 1, function ( assert ) { var hsl = $.colorUtil.rgbToHsl( 144, 238, 144 ); // Cross-browser differences in decimals... @@ -45,27 +36,23 @@ test( 'rgbToHsl', function() { // Re-create the rgbToHsl return array items, limited to two decimals. var ret = [dualDecimals(hsl[0]), dualDecimals(hsl[1]), dualDecimals(hsl[2])]; - deepEqual( ret, [0.33, 0.73, 0.75], 'rgb(144, 238, 144): hsl(0.33, 0.73, 0.75)' ); + assert.deepEqual( ret, [0.33, 0.73, 0.75], 'rgb(144, 238, 144): hsl(0.33, 0.73, 0.75)' ); }); -test( 'hslToRgb', function() { - expect(1); - +QUnit.test( 'hslToRgb', 1, function ( assert ) { var rgb = $.colorUtil.hslToRgb( 0.3, 0.7, 0.8 ); // Cross-browser differences in decimals... // Re-create the hslToRgb return array items, rounded to whole numbers. var ret = [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])]; - deepEqual( ret ,[183, 240, 168], 'hsl(0.3, 0.7, 0.8): rgb(183, 240, 168)' ); + assert.deepEqual( ret ,[183, 240, 168], 'hsl(0.3, 0.7, 0.8): rgb(183, 240, 168)' ); }); -test( 'getColorBrightness', function() { - expect(2); - +QUnit.test( 'getColorBrightness', 2, function ( assert ) { var a = $.colorUtil.getColorBrightness( 'red', +0.1 ); - equal( a, 'rgb(255,50,50)', 'Start with named color "red", brighten 10%' ); + assert.equal( a, 'rgb(255,50,50)', 'Start with named color "red", brighten 10%' ); var b = $.colorUtil.getColorBrightness( 'rgb(200,50,50)', -0.2 ); - equal( b, 'rgb(118,29,29)', 'Start with rgb string "rgb(200,50,50)", darken 20%' ); + assert.equal( b, 'rgb(118,29,29)', 'Start with rgb string "rgb(200,50,50)", darken 20%' ); }); diff --git a/tests/qunit/suites/resources/jquery/jquery.delayedBind.test.js b/tests/qunit/suites/resources/jquery/jquery.delayedBind.test.js index 6489a1f1..a3079835 100644 --- a/tests/qunit/suites/resources/jquery/jquery.delayedBind.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.delayedBind.test.js @@ -1,41 +1,35 @@ -test('jquery.delayedBind with data option', function() { +QUnit.asyncTest('jquery.delayedBind with data option', 2, function ( assert ) { var $fixture = $('<div>').appendTo('#qunit-fixture'), data = { magic: "beeswax" }, delay = 50; - $fixture.delayedBind(delay, 'testevent', data, function(event) { - start(); // continue! - ok(true, 'testevent fired'); - ok(event.data === data, 'data is passed through delayedBind'); + $fixture.delayedBind(delay, 'testevent', data, function ( e ) { + QUnit.start(); // continue! + assert.ok( true, 'testevent fired'); + assert.ok( e.data === data, 'data is passed through delayedBind'); }); - expect(2); - stop(); // async! - // We'll trigger it thrice, but it should only happen once. - $fixture.trigger('testevent', {}); - $fixture.trigger('testevent', {}); - $fixture.trigger('testevent', {}); - $fixture.trigger('testevent', {}); + $fixture.trigger( 'testevent', {} ); + $fixture.trigger( 'testevent', {} ); + $fixture.trigger( 'testevent', {} ); + $fixture.trigger( 'testevent', {} ); }); -test('jquery.delayedBind without data option', function() { +QUnit.asyncTest('jquery.delayedBind without data option', 1, function ( assert ) { var $fixture = $('<div>').appendTo('#qunit-fixture'), data = { magic: "beeswax" }, delay = 50; - $fixture.delayedBind(delay, 'testevent', function(event) { - start(); // continue! - ok(true, 'testevent fired'); + $fixture.delayedBind(delay, 'testevent', function ( e ) { + QUnit.start(); // continue! + assert.ok(true, 'testevent fired'); }); - expect(1); - stop(); // async! - // We'll trigger it thrice, but it should only happen once. - $fixture.trigger('testevent', {}); - $fixture.trigger('testevent', {}); - $fixture.trigger('testevent', {}); - $fixture.trigger('testevent', {}); + $fixture.trigger( 'testevent', {} ); + $fixture.trigger( 'testevent', {} ); + $fixture.trigger( 'testevent', {} ); + $fixture.trigger( 'testevent', {} ); }); diff --git a/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js b/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js index 9377a2f6..6eef1abb 100644 --- a/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js @@ -1,17 +1,11 @@ -module( 'jquery.getAttrs', QUnit.newMwEnvironment() ); +QUnit.module( 'jquery.getAttrs', QUnit.newMwEnvironment() ); -test( '-- Initial check', function() { - expect(1); - ok( $.fn.getAttrs, 'jQuery.fn.getAttrs defined' ); -} ); - -test( 'Check', function() { - expect(1); +QUnit.test( 'Check', 1, function ( assert ) { var attrs = { foo: 'bar', 'class': 'lorem' }, - $el = $( '<div>', attrs ); + $el = jQuery( '<div>', attrs ); - deepEqual( $el.getAttrs(), attrs, 'getAttrs() return object should match the attributes set, no more, no less' ); + assert.deepEqual( $el.getAttrs(), attrs, 'getAttrs() return object should match the attributes set, no more, no less' ); } ); diff --git a/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js b/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js index 4750d2b8..a94dca31 100644 --- a/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js @@ -1,12 +1,7 @@ -module( 'jquery.highlightText', QUnit.newMwEnvironment() ); +QUnit.module( 'jquery.highlightText', QUnit.newMwEnvironment() ); -test( '-- Initial check', function() { - expect(1); - ok( $.fn.highlightText, 'jQuery.fn.highlightText defined' ); -} ); - -test( 'Check', function() { - var cases = [ +QUnit.test( 'Check', function ( assert ) { + var $fixture, cases = [ { desc: 'Test 001', text: 'Blue Öyster Cult', @@ -224,16 +219,14 @@ test( 'Check', function() { expected: '<span class="highlight">بو</span>ل إيردوس' } ]; - expect(cases.length); - var $fixture; + QUnit.expect( cases.length ); - $.each(cases, function( i, item ) { - $fixture = $( '<p></p>' ).text( item.text ); - $fixture.highlightText( item.highlight ); - equals( + $.each( cases, function ( i, item ) { + $fixture = $( '<p>' ).text( item.text ).highlightText( item.highlight ); + assert.equal( $fixture.html(), - $('<p>' + item.expected + '</p>').html(), // re-parse to normalize! + $( '<p>' ).html( item.expected ).html(), // re-parse to normalize! item.desc || undefined - ); + ); } ); } ); diff --git a/tests/qunit/suites/resources/jquery/jquery.localize.test.js b/tests/qunit/suites/resources/jquery/jquery.localize.test.js index cd828634..c8e1d9f9 100644 --- a/tests/qunit/suites/resources/jquery/jquery.localize.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.localize.test.js @@ -1,13 +1,6 @@ -module( 'jquery.localize', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(1); - ok( $.fn.localize, 'jQuery.fn.localize defined' ); -} ); - -test( 'Handle basic replacements', function() { - expect(3); +QUnit.module( 'jquery.localize', QUnit.newMwEnvironment() ); +QUnit.test( 'Handle basic replacements', 4, function ( assert ) { var html, $lc; mw.messages.set( 'basic', 'Basic stuff' ); @@ -15,24 +8,28 @@ test( 'Handle basic replacements', function() { html = '<div><span><html:msg key="basic" /></span></div>'; $lc = $( html ).localize().find( 'span' ); - strictEqual( $lc.text(), 'Basic stuff', 'Tag: html:msg' ); + assert.strictEqual( $lc.text(), 'Basic stuff', 'Tag: html:msg' ); // Attribute: title-msg - html = '<div><span title-msg="basic" /></span></div>'; + html = '<div><span title-msg="basic"></span></div>'; $lc = $( html ).localize().find( 'span' ); - strictEqual( $lc.attr( 'title' ), 'Basic stuff', 'Attribute: title-msg' ); + assert.strictEqual( $lc.attr( 'title' ), 'Basic stuff', 'Attribute: title-msg' ); // Attribute: alt-msg - html = '<div><span alt-msg="basic" /></span></div>'; + html = '<div><span alt-msg="basic"></span></div>'; $lc = $( html ).localize().find( 'span' ); - strictEqual( $lc.attr( 'alt' ), 'Basic stuff', 'Attribute: alt-msg' ); -} ); + assert.strictEqual( $lc.attr( 'alt' ), 'Basic stuff', 'Attribute: alt-msg' ); -test( 'Proper escaping', function() { - expect(2); + // Attribute: placeholder-msg + html = '<div><input placeholder-msg="basic" /></div>'; + $lc = $( html ).localize().find( 'input' ); + assert.strictEqual( $lc.attr( 'placeholder' ), 'Basic stuff', 'Attribute: placeholder-msg' ); +} ); + +QUnit.test( 'Proper escaping', 2, function ( assert ) { var html, $lc; mw.messages.set( 'properfoo', '<proper esc="test">' ); @@ -40,21 +37,19 @@ test( 'Proper escaping', function() { // making sure it is actually using text() and attr() (or something with the same effect) // Text escaping - html = '<div><span><html:msg key="properfoo" /></span></div>'; + html = '<div><span><html:msg key="properfoo"></span></div>'; $lc = $( html ).localize().find( 'span' ); - strictEqual( $lc.text(), mw.msg( 'properfoo' ), 'Content is inserted as text, not as html.' ); + assert.strictEqual( $lc.text(), mw.msg( 'properfoo' ), 'Content is inserted as text, not as html.' ); // Attribute escaping - html = '<div><span title-msg="properfoo" /></span></div>'; + html = '<div><span title-msg="properfoo"></span></div>'; $lc = $( html ).localize().find( 'span' ); - strictEqual( $lc.attr( 'title' ), mw.msg( 'properfoo' ), 'Attributes are not inserted raw.' ); + assert.strictEqual( $lc.attr( 'title' ), mw.msg( 'properfoo' ), 'Attributes are not inserted raw.' ); } ); -test( 'Options', function() { - expect(7); - +QUnit.test( 'Options', 7, function ( assert ) { mw.messages.set( { 'foo-lorem': 'Lorem', 'foo-ipsum': 'Ipsum', @@ -67,17 +62,17 @@ test( 'Options', function() { var html, $lc, attrs, x, sitename = 'Wikipedia'; // Message key prefix - html = '<div><span title-msg="lorem"><html:msg key="ipsum" /></span></div>'; + html = '<div><span title-msg="lorem"><html:msg key="ipsum"></span></div>'; $lc = $( html ).localize( { prefix: 'foo-' } ).find( 'span' ); - strictEqual( $lc.attr( 'title' ), 'Lorem', 'Message key prefix - attr' ); - strictEqual( $lc.text(), 'Ipsum', 'Message key prefix - text' ); + assert.strictEqual( $lc.attr( 'title' ), 'Lorem', 'Message key prefix - attr' ); + assert.strictEqual( $lc.text(), 'Ipsum', 'Message key prefix - text' ); // Variable keys mapping x = 'bar'; - html = '<div><span title-msg="title"><html:msg key="label" /></span></div>'; + html = '<div><span title-msg="title"><html:msg key="label"></span></div>'; $lc = $( html ).localize( { keys: { 'title': 'foo-' + x + '-title', @@ -85,22 +80,22 @@ test( 'Options', function() { } } ).find( 'span' ); - strictEqual( $lc.attr( 'title' ), 'Read more about bars', 'Variable keys mapping - attr' ); - strictEqual( $lc.text(), 'The Bars', 'Variable keys mapping - text' ); + assert.strictEqual( $lc.attr( 'title' ), 'Read more about bars', 'Variable keys mapping - attr' ); + assert.strictEqual( $lc.text(), 'The Bars', 'Variable keys mapping - text' ); // Passing parameteters to mw.msg - html = '<div><span><html:msg key="foo-welcome" /></span></div>'; + html = '<div><span><html:msg key="foo-welcome"></span></div>'; $lc = $( html ).localize( { params: { 'foo-welcome': [sitename, 'yesterday'] } } ).find( 'span' ); - strictEqual( $lc.text(), 'Welcome to Wikipedia! (last visit: yesterday)', 'Passing parameteters to mw.msg' ); + assert.strictEqual( $lc.text(), 'Welcome to Wikipedia! (last visit: yesterday)', 'Passing parameteters to mw.msg' ); // Combination of options prefix, params and keys x = 'bazz'; - html = '<div><span title-msg="title"><html:msg key="label" /></span></div>'; + html = '<div><span title-msg="title"><html:msg key="label"></span></div>'; $lc = $( html ).localize( { prefix: 'foo-', keys: { @@ -114,6 +109,25 @@ test( 'Options', function() { } } ).find( 'span' ); - strictEqual( $lc.text(), 'The Bazz (Wikipedia)', 'Combination of options prefix, params and keys - text' ); - strictEqual( $lc.attr( 'title' ), 'Read more about bazz at Wikipedia (last modified: 3 minutes ago)', 'Combination of options prefix, params and keys - attr' ); + assert.strictEqual( $lc.text(), 'The Bazz (Wikipedia)', 'Combination of options prefix, params and keys - text' ); + assert.strictEqual( $lc.attr( 'title' ), 'Read more about bazz at Wikipedia (last modified: 3 minutes ago)', 'Combination of options prefix, params and keys - attr' ); +} ); + +QUnit.test( 'Handle data text', 2, function ( assert ) { + var html, $lc; + mw.messages.set( 'option-one', 'Item 1' ); + mw.messages.set( 'option-two', 'Item 2' ); + html = '<select><option data-msg-text="option-one"></option><option data-msg-text="option-two"></option></select>'; + $lc = $( html ).localize().find( 'option' ); + assert.strictEqual( $lc.eq( 0 ).text(), mw.msg( 'option-one' ), 'data-msg-text becomes text of options' ); + assert.strictEqual( $lc.eq( 1 ).text(), mw.msg( 'option-two' ), 'data-msg-text becomes text of options' ); +} ); + +QUnit.test( 'Handle data html', 2, function ( assert ) { + var html, $lc; + mw.messages.set( 'html', 'behold... there is a <a>link</a> here!!' ); + html = '<div><div data-msg-html="html"></div></div>'; + $lc = $( html ).localize().find( 'a' ); + assert.strictEqual( $lc.length, 1, 'link is created' ); + assert.strictEqual( $lc.text(), 'link', 'the link text got added' ); } ); diff --git a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js index 3a2d0d83..5b566ae0 100644 --- a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js @@ -1,58 +1,58 @@ -module( 'jquery.mwExtension', QUnit.newMwEnvironment() ); +QUnit.module( 'jquery.mwExtension', QUnit.newMwEnvironment() ); -test( 'String functions', function() { +QUnit.test( 'String functions', function ( assert ) { - equal( $.trimLeft( ' foo bar ' ), 'foo bar ', 'trimLeft' ); - equal( $.trimRight( ' foo bar ' ), ' foo bar', 'trimRight' ); - equal( $.ucFirst( 'foo' ), 'Foo', 'ucFirst' ); + assert.equal( $.trimLeft( ' foo bar ' ), 'foo bar ', 'trimLeft' ); + assert.equal( $.trimRight( ' foo bar ' ), ' foo bar', 'trimRight' ); + assert.equal( $.ucFirst( 'foo' ), 'Foo', 'ucFirst' ); - equal( $.escapeRE( '<!-- ([{+mW+}]) $^|?>' ), + assert.equal( $.escapeRE( '<!-- ([{+mW+}]) $^|?>' ), '<!\\-\\- \\(\\[\\{\\+mW\\+\\}\\]\\) \\$\\^\\|\\?>', 'escapeRE - Escape specials' ); - equal( $.escapeRE( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ), + assert.equal( $.escapeRE( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'escapeRE - Leave uppercase alone' ); - equal( $.escapeRE( 'abcdefghijklmnopqrstuvwxyz' ), + assert.equal( $.escapeRE( 'abcdefghijklmnopqrstuvwxyz' ), 'abcdefghijklmnopqrstuvwxyz', 'escapeRE - Leave lowercase alone' ); - equal( $.escapeRE( '0123456789' ), '0123456789', 'escapeRE - Leave numbers alone' ); + assert.equal( $.escapeRE( '0123456789' ), '0123456789', 'escapeRE - Leave numbers alone' ); }); -test( 'Is functions', function() { +QUnit.test( 'Is functions', function ( assert ) { - strictEqual( $.isDomElement( document.getElementById( 'qunit-header' ) ), true, + assert.strictEqual( $.isDomElement( document.getElementById( 'qunit-header' ) ), true, 'isDomElement: #qunit-header Node' ); - strictEqual( $.isDomElement( document.getElementById( 'random-name' ) ), false, + assert.strictEqual( $.isDomElement( document.getElementById( 'random-name' ) ), false, 'isDomElement: #random-name (null)' ); - strictEqual( $.isDomElement( document.getElementsByTagName( 'div' ) ), false, + assert.strictEqual( $.isDomElement( document.getElementsByTagName( 'div' ) ), false, 'isDomElement: getElementsByTagName Array' ); - strictEqual( $.isDomElement( document.getElementsByTagName( 'div' )[0] ), true, + assert.strictEqual( $.isDomElement( document.getElementsByTagName( 'div' )[0] ), true, 'isDomElement: getElementsByTagName(..)[0] Node' ); - strictEqual( $.isDomElement( $( 'div' ) ), false, + assert.strictEqual( $.isDomElement( $( 'div' ) ), false, 'isDomElement: jQuery object' ); - strictEqual( $.isDomElement( $( 'div' ).get(0) ), true, + assert.strictEqual( $.isDomElement( $( 'div' ).get(0) ), true, 'isDomElement: jQuery object > Get node' ); - strictEqual( $.isDomElement( document.createElement( 'div' ) ), true, + assert.strictEqual( $.isDomElement( document.createElement( 'div' ) ), true, 'isDomElement: createElement' ); - strictEqual( $.isDomElement( { foo: 1 } ), false, + assert.strictEqual( $.isDomElement( { foo: 1 } ), false, 'isDomElement: Object' ); - strictEqual( $.isEmpty( 'string' ), false, 'isEmptry: "string"' ); - strictEqual( $.isEmpty( '0' ), true, 'isEmptry: "0"' ); - strictEqual( $.isEmpty( '' ), true, 'isEmptry: ""' ); - strictEqual( $.isEmpty( 1 ), false, 'isEmptry: 1' ); - strictEqual( $.isEmpty( [] ), true, 'isEmptry: []' ); - strictEqual( $.isEmpty( {} ), true, 'isEmptry: {}' ); + assert.strictEqual( $.isEmpty( 'string' ), false, 'isEmptry: "string"' ); + assert.strictEqual( $.isEmpty( '0' ), true, 'isEmptry: "0"' ); + assert.strictEqual( $.isEmpty( '' ), true, 'isEmptry: ""' ); + assert.strictEqual( $.isEmpty( 1 ), false, 'isEmptry: 1' ); + assert.strictEqual( $.isEmpty( [] ), true, 'isEmptry: []' ); + assert.strictEqual( $.isEmpty( {} ), true, 'isEmptry: {}' ); // Documented behaviour - strictEqual( $.isEmpty( { length: 0 } ), true, 'isEmptry: { length: 0 }' ); + assert.strictEqual( $.isEmpty( { length: 0 } ), true, 'isEmptry: { length: 0 }' ); }); -test( 'Comparison functions', function() { +QUnit.test( 'Comparison functions', function ( assert ) { - ok( $.compareArray( [0, 'a', [], [2, 'b'] ], [0, "a", [], [2, "b"] ] ), + assert.ok( $.compareArray( [0, 'a', [], [2, 'b'] ], [0, "a", [], [2, "b"] ] ), 'compareArray: Two deep arrays that are excactly the same' ); - ok( !$.compareArray( [1], [2] ), 'compareArray: Two different arrays (false)' ); + assert.ok( !$.compareArray( [1], [2] ), 'compareArray: Two different arrays (false)' ); - ok( $.compareObject( {}, {} ), 'compareObject: Two empty objects' ); - ok( $.compareObject( { foo: 1 }, { foo: 1 } ), 'compareObject: Two the same objects' ); - ok( !$.compareObject( { bar: true }, { baz: false } ), + assert.ok( $.compareObject( {}, {} ), 'compareObject: Two empty objects' ); + assert.ok( $.compareObject( { foo: 1 }, { foo: 1 } ), 'compareObject: Two the same objects' ); + assert.ok( !$.compareObject( { bar: true }, { baz: false } ), 'compareObject: Two different objects (false)' ); }); diff --git a/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js b/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js index 98ff5508..161f0cd1 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js @@ -1,15 +1,6 @@ -module( 'jquery.tabIndex', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(2); - - ok( $.fn.firstTabIndex, '$.fn.firstTabIndex defined' ); - ok( $.fn.lastTabIndex, '$.fn.lastTabIndex defined' ); -}); - -test( 'firstTabIndex', function() { - expect(2); +QUnit.module( 'jquery.tabIndex', QUnit.newMwEnvironment() ); +QUnit.test( 'firstTabIndex', 2, function ( assert ) { var testEnvironment = '<form>' + '<input tabindex="7" />' + @@ -19,15 +10,13 @@ test( 'firstTabIndex', function() { '</form>'; var $testA = $( '<div>' ).html( testEnvironment ).appendTo( '#qunit-fixture' ); - strictEqual( $testA.firstTabIndex(), 2, 'First tabindex should be 2 within this context.' ); + assert.strictEqual( $testA.firstTabIndex(), 2, 'First tabindex should be 2 within this context.' ); var $testB = $( '<div>' ); - strictEqual( $testB.firstTabIndex(), null, 'Return null if none available.' ); + assert.strictEqual( $testB.firstTabIndex(), null, 'Return null if none available.' ); }); -test( 'lastTabIndex', function() { - expect(2); - +QUnit.test( 'lastTabIndex', 2, function ( assert ) { var testEnvironment = '<form>' + '<input tabindex="7" />' + @@ -37,8 +26,8 @@ test( 'lastTabIndex', function() { '</form>'; var $testA = $( '<div>' ).html( testEnvironment ).appendTo( '#qunit-fixture' ); - strictEqual( $testA.lastTabIndex(), 9, 'Last tabindex should be 9 within this context.' ); + assert.strictEqual( $testA.lastTabIndex(), 9, 'Last tabindex should be 9 within this context.' ); var $testB = $( '<div>' ); - strictEqual( $testB.lastTabIndex(), null, 'Return null if none available.' ); + assert.strictEqual( $testB.lastTabIndex(), null, 'Return null if none available.' ); }); diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js index 7ecdc4b1..16d81707 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -1,4 +1,4 @@ -( function () { +( function ( $, mw ) { var config = { wgMonthNames: ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], @@ -7,12 +7,7 @@ var config = { wgContentLanguage: 'en' }; -module( 'jquery.tablesorter', QUnit.newMwEnvironment( config ) ); - -test( '-- Initial check', function() { - expect(1); - ok( $.tablesorter, '$.tablesorter defined' ); -}); +QUnit.module( 'jquery.tablesorter', QUnit.newMwEnvironment({ config: config }) ); /** * Create an HTML table from an array of row arrays containing text strings. @@ -22,28 +17,29 @@ test( '-- Initial check', function() { * @param {String[][]} data * @return jQuery */ -var tableCreate = function( header, data ) { - var $table = $( '<table class="sortable"><thead></thead><tbody></tbody></table>' ), +function tableCreate( header, data ) { + var i, + $table = $( '<table class="sortable"><thead></thead><tbody></tbody></table>' ), $thead = $table.find( 'thead' ), $tbody = $table.find( 'tbody' ), $tr = $( '<tr>' ); - $.each( header, function( i, str ) { + $.each( header, function ( i, str ) { var $th = $( '<th>' ); $th.text( str ).appendTo( $tr ); }); $tr.appendTo( $thead ); - for (var i = 0; i < data.length; i++) { + for ( i = 0; i < data.length; i++ ) { $tr = $( '<tr>' ); - $.each( data[i], function( j, str ) { + $.each( data[i], function ( j, str ) { var $td = $( '<td>' ); $td.text( str ).appendTo( $tr ); }); $tr.appendTo( $tbody ); } return $table; -}; +} /** * Extract text from table. @@ -51,7 +47,7 @@ var tableCreate = function( header, data ) { * @param {jQuery} $table * @return String[][] */ -var tableExtract = function( $table ) { +function tableExtract( $table ) { var data = []; $table.find( 'tbody' ).find( 'tr' ).each( function( i, tr ) { @@ -62,7 +58,7 @@ var tableExtract = function( $table ) { data.push( row ); }); return data; -}; +} /** * Run a table test by building a table with the given data, @@ -74,10 +70,8 @@ var tableExtract = function( $table ) { * @param {String[][]} expected rows/cols to compare against at end * @param {function($table)} callback something to do with the table before we compare */ -var tableTest = function( msg, header, data, expected, callback ) { - test( msg, function() { - expect(1); - +function tableTest( msg, header, data, expected, callback ) { + QUnit.test( msg, 1, function ( assert ) { var $table = tableCreate( header, data ); // Give caller a chance to set up sorting and manipulate the table. @@ -86,15 +80,18 @@ var tableTest = function( msg, header, data, expected, callback ) { // Table sorting is done synchronously; if it ever needs to change back // to asynchronous, we'll need a timeout or a callback here. var extracted = tableExtract( $table ); - deepEqual( extracted, expected, msg ); + assert.deepEqual( extracted, expected, msg ); }); -}; +} -var reversed = function(arr) { +function reversed(arr) { + // Clone array var arr2 = arr.slice(0); + arr2.reverse(); + return arr2; -}; +} // Sample data set using planets named and their radius var header = [ 'Planet' , 'Radius (km)'], @@ -115,7 +112,7 @@ tableTest( header, planets, ascendingName, - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(0)' ).click(); } @@ -125,7 +122,7 @@ tableTest( header, planets, ascendingName, - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(0)' ).click(); } @@ -135,7 +132,7 @@ tableTest( header, planets, reversed(ascendingName), - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(0)' ).click().click(); } @@ -145,7 +142,7 @@ tableTest( header, planets, ascendingRadius, - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(1)' ).click(); } @@ -155,7 +152,7 @@ tableTest( header, planets, reversed(ascendingRadius), - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(1)' ).click().click(); } @@ -180,7 +177,7 @@ tableTest( ['09.11.2011'], ['11.11.2011'] ], - function( $table ) { + function ( $table ) { mw.config.set( 'wgDefaultDateFormat', 'dmy' ); mw.config.set( 'wgContentLanguage', 'de' ); @@ -206,7 +203,7 @@ tableTest( ['09.11.2011'], ['11.11.2011'] ], - function( $table ) { + function ( $table ) { mw.config.set( 'wgDefaultDateFormat', 'mdy' ); $table.tablesorter(); @@ -242,7 +239,7 @@ tableTest( ['IP'], ipv4, ipv4Sorted, - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(0)' ).click(); } @@ -252,7 +249,7 @@ tableTest( ['IP'], ipv4, reversed(ipv4Sorted), - function( $table ) { + function ( $table ) { $table.tablesorter(); $table.find( '.headerSort:eq(0)' ).click().click(); } @@ -287,7 +284,7 @@ tableTest( ['Name'], umlautWords, umlautWordsSorted, - function( $table ) { + function ( $table ) { mw.config.set( 'tableSorterCollation', { 'ä': 'ae', 'ö': 'oe', @@ -301,14 +298,14 @@ tableTest( ); var planetsRowspan = [["Earth","6051.8"], jupiter, ["Mars","6051.8"], mercury, saturn, venus]; -var planetsRowspanII = [jupiter, mercury, saturn, ['Venus', '6371.0'], venus, ['Venus', '3390.0']]; +var planetsRowspanII = [jupiter, mercury, saturn, venus, ['Venus', '6371.0'], ['Venus', '3390.0']]; tableTest( 'Basic planet table: same value for multiple rows via rowspan', header, planets, planetsRowspan, - function( $table ) { + function ( $table ) { // Modify the table to have a multiuple-row-spanning cell: // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row. $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove(); @@ -325,7 +322,7 @@ tableTest( header, planets, planetsRowspanII, - function( $table ) { + function ( $table ) { // Modify the table to have a multiuple-row-spanning cell: // - Remove 1st cell of 4th row, and, 1st cell or 5th row. $table.find( 'tr:eq(3) td:eq(0), tr:eq(4) td:eq(0)' ).remove(); @@ -348,11 +345,11 @@ var complexMDYDates = [ ]; var complexMDYSorted = [ - ["5.12.1990"], - ["April 21 1991"], - ["04 22 1991"], - ["January, 19 2010"], - ["December 12 '10"] + ['5.12.1990'], + ['April 21 1991'], + ['04 22 1991'], + ['January, 19 2010'], + ['December 12 \'10'] ]; tableTest( @@ -360,7 +357,7 @@ tableTest( ['date'], complexMDYDates, complexMDYSorted, - function( $table ) { + function ( $table ) { mw.config.set( 'wgDefaultDateFormat', 'mdy' ); $table.tablesorter(); @@ -368,6 +365,39 @@ tableTest( } ); +var currencyUnsorted = [ + ['1.02 $'], + ['$ 3.00'], + ['€ 2,99'], + ['$ 1.00'], + ['$3.50'], + ['$ 1.50'], + ['€ 0.99'] +]; + +var currencySorted = [ + ['€ 0.99'], + ['$ 1.00'], + ['1.02 $'], + ['$ 1.50'], + ['$ 3.00'], + ['$3.50'], + // Comma's sort after dots + // Not intentional but test to detect changes + ['€ 2,99'] +]; + +tableTest( + 'Currency parsing I', + ['currency'], + currencyUnsorted, + currencySorted, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } +); + var ascendingNameLegacy = ascendingName.slice(0); ascendingNameLegacy[4] = ascendingNameLegacy[5]; ascendingNameLegacy.pop(); @@ -384,8 +414,9 @@ tableTest( } ); + /** FIXME: the diff output is not very readeable. */ -test( 'bug 32047 - caption must be before thead', function() { +QUnit.test( 'bug 32047 - caption must be before thead', function ( assert ) { var $table; $table = $( '<table class="sortable">' + @@ -398,17 +429,18 @@ test( 'bug 32047 - caption must be before thead', function() { ); $table.tablesorter(); - equals( + assert.equal( $table.children( ).get( 0 ).nodeName, 'CAPTION', 'First element after <thead> must be <caption> (bug 32047)' ); }); -test( 'data-sort-value attribute, when available, should override sorting position', function() { +QUnit.test( 'data-sort-value attribute, when available, should override sorting position', function ( assert ) { var $table, data; - // Simple example, one without data-sort-value which should be sorted at it's text. + // Example 1: All cells except one cell without data-sort-value, + // which should be sorted at it's text content value. $table = $( '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' + '<tbody>' + @@ -424,30 +456,33 @@ test( 'data-sort-value attribute, when available, should override sorting positi data = []; $table.find( 'tbody > tr' ).each( function( i, tr ) { $( tr ).find( 'td' ).each( function( i, td ) { - data.push( { data: $( td ).data( 'sort-value' ), text: $( td ).text() } ); + data.push( { + data: $( td ).data( 'sortValue' ), + text: $( td ).text() + } ); }); }); - deepEqual( data, [ + assert.deepEqual( data, [ { - "data": "Apple", - "text": "Bird" + data: 'Apple', + text: 'Bird' }, { - "data": "Bananna", - "text": "Ferret" + data: 'Bananna', + text: 'Ferret' }, { - "data": undefined, - "text": "Cheetah" + data: undefined, + text: 'Cheetah' }, { - "data": "Cherry", - "text": "Dolphin" + data: 'Cherry', + text: 'Dolphin' }, { - "data": "Drupe", - "text": "Elephant" + data: 'Drupe', + text: 'Elephant' } - ] ); + ], 'Order matches expected order (based on data-sort-value attribute values)' ); - // Another example + // Example 2 $table = $( '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' + '<tbody>' + @@ -461,30 +496,91 @@ test( 'data-sort-value attribute, when available, should override sorting positi $table.tablesorter().find( '.headerSort:eq(0)' ).click(); data = []; + $table.find( 'tbody > tr' ).each( function ( i, tr ) { + $( tr ).find( 'td' ).each( function ( i, td ) { + data.push( { + data: $( td ).data( 'sortValue' ), + text: $( td ).text() + } ); + }); + }); + + assert.deepEqual( data, [ + { + data: undefined, + text: 'B' + }, { + data: undefined, + text: 'D' + }, { + data: 'E', + text: 'A' + }, { + data: 'F', + text: 'C' + }, { + data: undefined, + text: 'G' + } + ], 'Order matches expected order (based on data-sort-value attribute values)' ); + + // Example 3: Test that live changes are used from data-sort-value, + // even if they change after the tablesorter is constructed (bug 38152). + $table = $( + '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' + + '<tbody>' + + '<tr><td>D</td></tr>' + + '<tr><td data-sort-value="1">A</td></tr>' + + '<tr><td>B</td></tr>' + + '<tr><td data-sort-value="2">G</td></tr>' + + '<tr><td>C</td></tr>' + + '</tbody></table>' + ); + // initialize table sorter and sort once + $table + .tablesorter() + .find( '.headerSort:eq(0)' ).click(); + + // Change the sortValue data properties (bug 38152) + // - change data + $table.find( 'td:contains(A)' ).data( 'sortValue', 3 ); + // - add data + $table.find( 'td:contains(B)' ).data( 'sortValue', 1 ); + // - remove data, bring back attribute: 2 + $table.find( 'td:contains(G)' ).removeData( 'sortValue' ); + + // Now sort again (twice, so it is back at Ascending) + $table.find( '.headerSort:eq(0)' ).click(); + $table.find( '.headerSort:eq(0)' ).click(); + + data = []; $table.find( 'tbody > tr' ).each( function( i, tr ) { $( tr ).find( 'td' ).each( function( i, td ) { - data.push( { data: $( td ).data( 'sort-value' ), text: $( td ).text() } ); + data.push( { + data: $( td ).data( 'sortValue' ), + text: $( td ).text() + } ); }); }); - deepEqual( data, [ + assert.deepEqual( data, [ { - "data": undefined, - "text": "B" + data: 1, + text: "B" }, { - "data": undefined, - "text": "D" + data: 2, + text: "G" }, { - "data": "E", - "text": "A" + data: 3, + text: "A" }, { - "data": "F", - "text": "C" + data: undefined, + text: "C" }, { - "data": undefined, - "text": "G" + data: undefined, + text: "D" } - ] ); + ], 'Order matches expected order, using the current sortValue in $.data()' ); }); @@ -522,13 +618,11 @@ tableTest( 'bug 8115: sort numbers with commas (descending)', ); // TODO add numbers sorting tests for bug 8115 with a different language -test( 'bug 32888 - Tables inside a tableheader cell', function() { - expect(2); - +QUnit.test( 'bug 32888 - Tables inside a tableheader cell', 2, function ( assert ) { var $table; $table = $( - '<table class="sortable" id="32888">' + - '<tr><th>header<table id="32888-2">'+ + '<table class="sortable" id="mw-bug-32888">' + + '<tr><th>header<table id="mw-bug-32888-2">'+ '<tr><th>1</th><th>2</th></tr>' + '</table></th></tr>' + '<tr><td>A</td></tr>' + @@ -537,16 +631,67 @@ test( 'bug 32888 - Tables inside a tableheader cell', function() { ); $table.tablesorter(); - equals( + assert.equal( $table.find('> thead:eq(0) > tr > th.headerSort').length, 1, 'Child tables inside a headercell should not interfere with sortable headers (bug 32888)' ); - equals( - $('#32888-2').find('th.headerSort').length, + assert.equal( + $( '#mw-bug-32888-2' ).find('th.headerSort').length, 0, 'The headers of child tables inside a headercell should not be sortable themselves (bug 32888)' ); }); -})(); + +var correctDateSorting1 = [ + ['01 January 2010'], + ['05 February 2010'], + ['16 January 2010'] +]; + +var correctDateSortingSorted1 = [ + ['01 January 2010'], + ['16 January 2010'], + ['05 February 2010'] +]; + +tableTest( + 'Correct date sorting I', + ['date'], + correctDateSorting1, + correctDateSortingSorted1, + function ( $table ) { + mw.config.set( 'wgDefaultDateFormat', 'mdy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } +); + +var correctDateSorting2 = [ + ['January 01 2010'], + ['February 05 2010'], + ['January 16 2010'] +]; + +var correctDateSortingSorted2 = [ + ['January 01 2010'], + ['January 16 2010'], + ['February 05 2010'] +]; + +tableTest( + 'Correct date sorting II', + ['date'], + correctDateSorting2, + correctDateSortingSorted2, + function ( $table ) { + mw.config.set( 'wgDefaultDateFormat', 'dmy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } +); + +}( jQuery, mediaWiki ) ); diff --git a/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js b/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js index 1b2f3024..f0a210f5 100644 --- a/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js @@ -1,9 +1,4 @@ -module( 'jquery.textSelection', QUnit.newMwEnvironment() ); - -test( '-- Initial check', function() { - expect(1); - ok( $.fn.textSelection, 'jQuery.fn.textSelection defined' ); -} ); +QUnit.module( 'jquery.textSelection', QUnit.newMwEnvironment() ); /** * Test factory for $.fn.textSelection( 'encapsulateText' ) @@ -16,7 +11,7 @@ test( '-- Initial check', function() { * end {int} ending char for selection * params {object} add'l parameters for $().textSelection( 'encapsulateText' ) */ -var encapsulateTest = function( options ) { +function encapsulateTest( options ) { var opt = $.extend({ description: '', before: {}, @@ -34,12 +29,12 @@ var encapsulateTest = function( options ) { selected: null }, opt.after); - test( opt.description, function() { + QUnit.test( opt.description, function ( assert ) { var tests = 1; if ( opt.after.selected !== null ) { tests++; } - expect( tests ); + QUnit.expect( tests ); var $textarea = $( '<textarea>' ); @@ -65,15 +60,15 @@ var encapsulateTest = function( options ) { var text = $textarea.textSelection( 'getContents' ).replace( /\r\n/g, "\n" ); - equal( text, opt.after.text, 'Checking full text after encapsulation' ); + assert.equal( text, opt.after.text, 'Checking full text after encapsulation' ); if (opt.after.selected !== null) { var selected = $textarea.textSelection( 'getSelection' ); - equal( selected, opt.after.selected, 'Checking selected text after encapsulation.' ); + assert.equal( selected, opt.after.selected, 'Checking selected text after encapsulation.' ); } } ); -}; +} var sig = { 'pre': "--~~~~" @@ -86,7 +81,7 @@ var sig = { 'peri': 'Heading 2', 'post': ' ==', 'regex': /^(\s*)(={1,6})(.*?)\2(\s*)$/, - 'regexReplace': "\$1==\$3==\$4", + 'regexReplace': "$1==$3==$4", 'ownline': true }, ulist = { 'pre': "* ", @@ -222,28 +217,26 @@ encapsulateTest({ }); -var caretTest = function(options) { - test(options.description, function() { - expect(2); - - var $textarea = $( '<textarea>' ).text(options.text); +function caretTest( options ) { + QUnit.test( options.description, 2, function ( assert ) { + var $textarea = $( '<textarea>' ).text( options.text ); $( '#qunit-fixture' ).append( $textarea ); - if (options.mode == 'set') { + if ( options.mode === 'set' ) { $textarea.textSelection('setSelection', { start: options.start, end: options.end }); } - var among = function(actual, expected, message) { - if ($.isArray(expected)) { - ok($.inArray(actual, expected) !== -1 , message + ' (got ' + actual + '; expected one of ' + expected.join(', ') + ')'); + function among( actual, expected, message ) { + if ( $.isArray( expected ) ) { + assert.ok( $.inArray( actual, expected ) !== -1 , message + ' (got ' + actual + '; expected one of ' + expected.join(', ') + ')' ); } else { - equal(actual, expected, message); + assert.equal( actual, expected, message ); } - }; + } var pos = $textarea.textSelection('getCaretPosition', {startAndEnd: true}); among(pos[0], options.start, 'Caret start should be where we set it.'); @@ -253,6 +246,8 @@ var caretTest = function(options) { var caretSample = "Some big text that we like to work with. Nothing fancy... you know what I mean?"; +/* + // @broken: Disabled per bug 34820 caretTest({ description: 'getCaretPosition with original/empty selection - bug 31847 with IE 6/7/8', text: caretSample, @@ -260,6 +255,7 @@ caretTest({ end: [0, caretSample.length], // Other browsers default it to the beginning (0), so check both. mode: 'get' }); +*/ caretTest({ description: 'set/getCaretPosition with forced empty selection', diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js new file mode 100644 index 00000000..3d3f630f --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js @@ -0,0 +1,26 @@ +QUnit.module( 'mediawiki.api.parse', QUnit.newMwEnvironment() ); + +QUnit.asyncTest( 'Hello world', function ( assert ) { + var api; + QUnit.expect( 6 ); + + api = new mw.Api(); + + api.parse( "'''Hello world'''" ) + .done( function ( html ) { + // Parse into a document fragment instead of comparing HTML, due to + // presence of Tidy influencing whitespace. + // Html also contains "NewPP report" comment. + var $res = $( '<div>' ).html( html ).children(), + res = $res.get( 0 ); + assert.equal( $res.length, 1, 'Response contains 1 element' ); + assert.equal( res.nodeName.toLowerCase(), 'p', 'Response is a paragraph' ); + assert.equal( $res.children().length, 1, 'Response has 1 child element' ); + assert.equal( $res.children().get( 0 ).nodeName.toLowerCase(), 'b', 'Child element is a bold tag' ); + // Trim since Tidy may or may not mess with the spacing here + assert.equal( $.trim( $res.text() ), 'Hello world', 'Response contains given text' ); + assert.equal( $res.find( 'b' ).text(), 'Hello world', 'Bold tag wraps the entire, same, text' ); + + QUnit.start(); + }); +}); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js new file mode 100644 index 00000000..79bd7306 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js @@ -0,0 +1,59 @@ +QUnit.module( 'mediawiki.api', QUnit.newMwEnvironment() ); + +QUnit.asyncTest( 'Basic functionality', function ( assert ) { + var api, d1, d2, d3; + QUnit.expect( 3 ); + + api = new mw.Api(); + + d1 = api.get( {} ) + .done( function ( data ) { + assert.deepEqual( data, [], 'If request succeeds without errors, resolve deferred' ); + }); + + d2 = api.get({ + action: 'doesntexist' + }) + .fail( function ( errorCode, details ) { + assert.equal( errorCode, 'unknown_action', 'API error (e.g. "unknown_action") should reject the deferred' ); + }); + + d3 = api.post( {} ) + .done( function ( data ) { + assert.deepEqual( data, [], 'Simple POST request' ); + }); + + // After all are completed, continue the test suite. + QUnit.whenPromisesComplete( d1, d2, d3 ).always( function () { + QUnit.start(); + }); +}); + +QUnit.asyncTest( 'Deprecated callback methods', function ( assert ) { + var api, d1, d2, d3; + QUnit.expect( 3 ); + + api = new mw.Api(); + + d1 = api.get( {}, function () { + assert.ok( true, 'Function argument treated as success callback.' ); + }); + + d2 = api.get( {}, { + ok: function ( data ) { + assert.ok( true, '"ok" property treated as success callback.' ); + } + }); + + d3 = api.get({ + action: 'doesntexist' + }, { + err: function ( data ) { + assert.ok( true, '"err" property treated as error callback.' ); + } + }); + + QUnit.whenPromisesComplete( d1, d2, d3 ).always( function () { + QUnit.start(); + }); +}); diff --git a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js index d73fe5a6..7fe7baf8 100644 --- a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js +++ b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js @@ -1,13 +1,8 @@ -module( 'mediawiki.special.recentchanges', QUnit.newMwEnvironment() ); +QUnit.module( 'mediawiki.special.recentchanges', QUnit.newMwEnvironment() ); -test( '-- Initial check', function() { - expect( 2 ); - ok( mw.special.recentchanges.init, 'mw.special.recentchanges.init defined' ); - ok( mw.special.recentchanges.updateCheckboxes, 'mw.special.recentchanges.updateCheckboxes defined' ); - // TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ] -}); +// TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ] -test( '"all" namespace disable checkboxes', function() { +QUnit.test( '"all" namespace disable checkboxes', function ( assert ) { // from Special:Recentchanges var select = @@ -33,15 +28,15 @@ test( '"all" namespace disable checkboxes', function() { // TODO abstract the double strictEquals // At first checkboxes are enabled - strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false ); - strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false ); + assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false ); + assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false ); // Initiate the recentchanges module mw.special.recentchanges.init(); // By default - strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true ); - strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true ); + assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true ); + assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true ); // select second option... var $options = $( '#namespace' ).find( 'option' ); @@ -50,8 +45,8 @@ test( '"all" namespace disable checkboxes', function() { $( '#namespace' ).change(); // ... and checkboxes should be enabled again - strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false ); - strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false ); + assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false ); + assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false ); // select first option ( 'all' namespace)... $options.eq(1).removeProp( 'selected' ); @@ -59,8 +54,8 @@ test( '"all" namespace disable checkboxes', function() { $( '#namespace' ).change(); // ... and checkboxes should now be disabled - strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true ); - strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true ); + assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true ); + assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true ); // DOM cleanup $env.remove(); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js index e04111f1..a736e121 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js @@ -55,147 +55,146 @@ var config = { "wgCaseSensitiveNamespaces": [] }; -module( 'mediawiki.Title', QUnit.newMwEnvironment( config ) ); +QUnit.module( 'mediawiki.Title', QUnit.newMwEnvironment({ config: config }) ); -test( '-- Initial check', function () { - expect(1); - ok( mw.Title, 'mw.Title defined' ); -}); - -test( 'Transformation', function () { - expect(8); +QUnit.test( 'Transformation', 8, function ( assert ) { var title; title = new mw.Title( 'File:quux pif.jpg' ); - equal( title.getName(), 'Quux_pif' ); + assert.equal( title.getName(), 'Quux_pif' ); title = new mw.Title( 'File:Glarg_foo_glang.jpg' ); - equal( title.getNameText(), 'Glarg foo glang' ); + assert.equal( title.getNameText(), 'Glarg foo glang' ); title = new mw.Title( 'User:ABC.DEF' ); - equal( title.toText(), 'User:ABC.DEF' ); - equal( title.getNamespaceId(), 2 ); - equal( title.getNamespacePrefix(), 'User:' ); + assert.equal( title.toText(), 'User:ABC.DEF' ); + assert.equal( title.getNamespaceId(), 2 ); + assert.equal( title.getNamespacePrefix(), 'User:' ); title = new mw.Title( 'uSEr:hAshAr' ); - equal( title.toText(), 'User:HAshAr' ); - equal( title.getNamespaceId(), 2 ); + assert.equal( title.toText(), 'User:HAshAr' ); + assert.equal( title.getNamespaceId(), 2 ); title = new mw.Title( ' MediaWiki: Foo bar .js ' ); // Don't ask why, it's the way the backend works. One space is kept of each set - equal( title.getName(), 'Foo_bar_.js', "Merge multiple spaces to a single space." ); + assert.equal( title.getName(), 'Foo_bar_.js', "Merge multiple spaces to a single space." ); }); -test( 'Main text for filename', function () { - expect(8); - +QUnit.test( 'Main text for filename', 8, function ( assert ) { var title = new mw.Title( 'File:foo_bar.JPG' ); - equal( title.getNamespaceId(), 6 ); - equal( title.getNamespacePrefix(), 'File:' ); - equal( title.getName(), 'Foo_bar' ); - equal( title.getNameText(), 'Foo bar' ); - equal( title.getMain(), 'Foo_bar.JPG' ); - equal( title.getMainText(), 'Foo bar.JPG' ); - equal( title.getExtension(), 'JPG' ); - equal( title.getDotExtension(), '.JPG' ); + assert.equal( title.getNamespaceId(), 6 ); + assert.equal( title.getNamespacePrefix(), 'File:' ); + assert.equal( title.getName(), 'Foo_bar' ); + assert.equal( title.getNameText(), 'Foo bar' ); + assert.equal( title.getMain(), 'Foo_bar.JPG' ); + assert.equal( title.getMainText(), 'Foo bar.JPG' ); + assert.equal( title.getExtension(), 'JPG' ); + assert.equal( title.getDotExtension(), '.JPG' ); }); -test( 'Namespace detection and conversion', function () { - expect(6); - +QUnit.test( 'Namespace detection and conversion', 6, function ( assert ) { var title; title = new mw.Title( 'something.PDF', 6 ); - equal( title.toString(), 'File:Something.PDF' ); + assert.equal( title.toString(), 'File:Something.PDF' ); title = new mw.Title( 'NeilK', 3 ); - equal( title.toString(), 'User_talk:NeilK' ); - equal( title.toText(), 'User talk:NeilK' ); + assert.equal( title.toString(), 'User_talk:NeilK' ); + assert.equal( title.toText(), 'User talk:NeilK' ); title = new mw.Title( 'Frobisher', 100 ); - equal( title.toString(), 'Penguins:Frobisher' ); + assert.equal( title.toString(), 'Penguins:Frobisher' ); title = new mw.Title( 'antarctic_waterfowl:flightless_yet_cute.jpg' ); - equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' ); + assert.equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' ); title = new mw.Title( 'Penguins:flightless_yet_cute.jpg' ); - equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' ); + assert.equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' ); }); -test( 'Throw error on invalid title', function () { - expect(1); - - raises(function () { +QUnit.test( 'Throw error on invalid title', 1, function ( assert ) { + assert.throws(function () { var title = new mw.Title( '' ); }, 'Throw error on empty string' ); }); -test( 'Case-sensivity', function () { - expect(3); - +QUnit.test( 'Case-sensivity', 3, function ( assert ) { var title; // Default config mw.config.set( 'wgCaseSensitiveNamespaces', [] ); title = new mw.Title( 'article' ); - equal( title.toString(), 'Article', 'Default config: No sensitive namespaces by default. First-letter becomes uppercase' ); + assert.equal( title.toString(), 'Article', 'Default config: No sensitive namespaces by default. First-letter becomes uppercase' ); // $wgCapitalLinks = false; mw.config.set( 'wgCaseSensitiveNamespaces', [0, -2, 1, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15] ); title = new mw.Title( 'article' ); - equal( title.toString(), 'article', '$wgCapitalLinks=false: Article namespace is sensitive, first-letter case stays lowercase' ); + assert.equal( title.toString(), 'article', '$wgCapitalLinks=false: Article namespace is sensitive, first-letter case stays lowercase' ); title = new mw.Title( 'john', 2 ); - equal( title.toString(), 'User:John', '$wgCapitalLinks=false: User namespace is insensitive, first-letter becomes uppercase' ); + assert.equal( title.toString(), 'User:John', '$wgCapitalLinks=false: User namespace is insensitive, first-letter becomes uppercase' ); }); -test( 'toString / toText', function () { - expect(2); - +QUnit.test( 'toString / toText', 2, function ( assert ) { var title = new mw.Title( 'Some random page' ); - equal( title.toString(), title.getPrefixedDb() ); - equal( title.toText(), title.getPrefixedText() ); + assert.equal( title.toString(), title.getPrefixedDb() ); + assert.equal( title.toText(), title.getPrefixedText() ); }); -test( 'Exists', function () { - expect(3); +QUnit.test( 'getExtension', 7, function ( assert ) { + + function extTest( pagename, ext, description ) { + var title = new mw.Title( pagename ); + assert.equal( title.getExtension(), ext, description || pagename ); + } + + extTest( 'MediaWiki:Vector.js', 'js' ); + extTest( 'User:Example/common.css', 'css' ); + extTest( 'File:Example.longextension', 'longextension', 'Extension parsing not limited (bug 36151)' ); + extTest( 'Example/information.json', 'json', 'Extension parsing not restricted from any namespace' ); + extTest( 'Foo.', null, 'Trailing dot is not an extension' ); + extTest( 'Foo..', null, 'Trailing dots are not an extension' ); + extTest( 'Foo.a.', null, 'Page name with dots and ending in a dot does not have an extension' ); + // @broken: Throws an exception + // extTest( '.NET', null, 'Leading dot is (or is not?) an extension' ); +}); + +QUnit.test( 'exists', 3, function ( assert ) { var title; // Empty registry, checks default to null title = new mw.Title( 'Some random page', 4 ); - strictEqual( title.exists(), null, 'Return null with empty existance registry' ); + assert.strictEqual( title.exists(), null, 'Return null with empty existance registry' ); // Basic registry, checks default to boolean mw.Title.exist.set( ['Does_exist', 'User_talk:NeilK', 'Wikipedia:Sandbox_rules'], true ); mw.Title.exist.set( ['Does_not_exist', 'User:John', 'Foobar'], false ); title = new mw.Title( 'Project:Sandbox rules' ); - assertTrue( title.exists(), 'Return true for page titles marked as existing' ); + assert.assertTrue( title.exists(), 'Return true for page titles marked as existing' ); title = new mw.Title( 'Foobar' ); - assertFalse( title.exists(), 'Return false for page titles marked as nonexistent' ); + assert.assertFalse( title.exists(), 'Return false for page titles marked as nonexistent' ); }); -test( 'Url', function () { - expect(2); - +QUnit.test( 'getUrl', 2, function ( assert ) { var title; // Config mw.config.set( 'wgArticlePath', '/wiki/$1' ); title = new mw.Title( 'Foobar' ); - equal( title.getUrl(), '/wiki/Foobar', 'Basic functionally, toString passing to wikiGetlink' ); + assert.equal( title.getUrl(), '/wiki/Foobar', 'Basic functionally, toString passing to wikiGetlink' ); title = new mw.Title( 'John Doe', 3 ); - equal( title.getUrl(), '/wiki/User_talk:John_Doe', 'Escaping in title and namespace for urls' ); + assert.equal( title.getUrl(), '/wiki/User_talk:John_Doe', 'Escaping in title and namespace for urls' ); }); }() );
\ No newline at end of file diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js new file mode 100644 index 00000000..68a9eafb --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js @@ -0,0 +1,388 @@ +QUnit.module( 'mediawiki.Uri', QUnit.newMwEnvironment({ + setup: function () { + this.mwUriOrg = mw.Uri; + mw.Uri = mw.UriRelative( 'http://example.org/w/index.php' ); + }, + teardown: function () { + mw.Uri = this.mwUriOrg; + delete this.mwUriOrg; + } +}) ); + +$.each( [true, false], function ( i, strictMode ) { + QUnit.test( 'Basic mw.Uri object test in ' + ( strictMode ? '' : 'non-' ) + 'strict mode for a simple HTTP URI', 2, function ( assert ) { + var uriString, uri; + uriString = 'http://www.ietf.org/rfc/rfc2396.txt'; + uri = new mw.Uri( uriString, { + strictMode: strictMode + }); + + assert.deepEqual( + { + protocol: uri.protocol, + host: uri.host, + port: uri.port, + path: uri.path, + query: uri.query, + fragment: uri.fragment + }, { + protocol: 'http', + host: 'www.ietf.org', + port: undefined, + path: '/rfc/rfc2396.txt', + query: {}, + fragment: undefined + }, + 'basic object properties' + ); + + assert.deepEqual( + { + userInfo: uri.getUserInfo(), + authority: uri.getAuthority(), + hostPort: uri.getHostPort(), + queryString: uri.getQueryString(), + relativePath: uri.getRelativePath(), + toString: uri.toString() + }, + { + userInfo: '', + authority: 'www.ietf.org', + hostPort: 'www.ietf.org', + queryString: '', + relativePath: '/rfc/rfc2396.txt', + toString: uriString + }, + 'construct composite components of URI on request' + ); + + }); +}); + +QUnit.test( 'Parse an ftp URI correctly with user and password', 1, function ( assert ) { + var uri = new mw.Uri( 'ftp://usr:pwd@192.0.2.16/' ); + + assert.deepEqual( + { + protocol: uri.protocol, + user: uri.user, + password: uri.password, + host: uri.host, + port: uri.port, + path: uri.path, + query: uri.query, + fragment: uri.fragment + }, + { + protocol: 'ftp', + user: 'usr', + password: 'pwd', + host: '192.0.2.16', + port: undefined, + path: '/', + query: {}, + fragment: undefined + }, + 'basic object properties' + ); +} ); + +QUnit.test( 'Parse a uri with simple querystring', 1, function ( assert ) { + var uri = new mw.Uri( 'http://www.google.com/?q=uri' ); + + assert.deepEqual( + { + protocol: uri.protocol, + host: uri.host, + port: uri.port, + path: uri.path, + query: uri.query, + fragment: uri.fragment, + queryString: uri.getQueryString() + }, + { + protocol: 'http', + host: 'www.google.com', + port: undefined, + path: '/', + query: { q: 'uri' }, + fragment: undefined, + queryString: 'q=uri' + }, + 'basic object properties' + ); +} ); + +QUnit.test( 'Handle multiple query parameter (overrideKeys on)', 5, function ( assert ) { + var uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', { + overrideKeys: true + }); + + assert.equal( uri.query.n, '1', 'multiple parameters are parsed' ); + assert.equal( uri.query.m, 'bar', 'last key overrides earlier keys' ); + + uri.query.n = [ 'x', 'y', 'z' ]; + + // Verify parts and total length instead of entire string because order + // of iteration can vary. + assert.ok( uri.toString().indexOf( 'm=bar' ), 'toString preserves other values' ); + assert.ok( uri.toString().indexOf( 'n=x&n=y&n=z' ), 'toString parameter includes all values of an array query parameter' ); + assert.equal( uri.toString().length, 'http://www.example.com/dir/?m=bar&n=x&n=y&n=z'.length, 'toString matches expected string' ); +} ); + +QUnit.test( 'Handle multiple query parameter (overrideKeys off)', 9, function ( assert ) { + var uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', { + overrideKeys: false + }); + + // Strict comparison so that types are also verified (n should be string '1') + assert.strictEqual( uri.query.m.length, 2, 'multi-value query should be an array with 2 items' ); + assert.strictEqual( uri.query.m[0], 'foo', 'order and value is correct' ); + assert.strictEqual( uri.query.m[1], 'bar', 'order and value is correct' ); + assert.strictEqual( uri.query.n, '1', 'n=1 is parsed with the correct value of the expected type' ); + + // Change query values + uri.query.n = [ 'x', 'y', 'z' ]; + + // Verify parts and total length instead of entire string because order + // of iteration can vary. + assert.ok( uri.toString().indexOf( 'm=foo&m=bar' ) >= 0, 'toString preserves other values' ); + assert.ok( uri.toString().indexOf( 'n=x&n=y&n=z' ) >= 0, 'toString parameter includes all values of an array query parameter' ); + assert.equal( uri.toString().length, 'http://www.example.com/dir/?m=foo&m=bar&n=x&n=y&n=z'.length, 'toString matches expected string' ); + + // Remove query values + uri.query.m.splice( 0, 1 ); + delete uri.query.n; + + assert.equal( uri.toString(), 'http://www.example.com/dir/?m=bar', 'deletion properties' ); + + // Remove more query values, leaving an empty array + uri.query.m.splice( 0, 1 ); + assert.equal( uri.toString(), 'http://www.example.com/dir/', 'empty array value is ommitted' ); +} ); + +QUnit.test( 'All-dressed URI with everything', 11, function ( assert ) { + var uri, queryString, relativePath; + + uri = new mw.Uri( 'http://auth@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=value+%28escaped%29#top' ); + + assert.deepEqual( + { + protocol: uri.protocol, + user: uri.user, + password: uri.password, + host: uri.host, + port: uri.port, + path: uri.path, + query: uri.query, + fragment: uri.fragment + }, + { + protocol: 'http', + user: 'auth', + password: undefined, + host: 'www.example.com', + port: '81', + path: '/dir/dir.2/index.htm', + query: { q1: '0', test1: null, test2: 'value (escaped)' }, + fragment: 'top' + }, + 'basic object properties' + ); + + assert.equal( uri.getUserInfo(), 'auth', 'user info' ); + + assert.equal( uri.getAuthority(), 'auth@www.example.com:81', 'authority equal to auth@hostport' ); + + assert.equal( uri.getHostPort(), 'www.example.com:81', 'hostport equal to host:port' ); + + queryString = uri.getQueryString(); + assert.ok( queryString.indexOf( 'q1=0' ) >= 0, 'query param with numbers' ); + assert.ok( queryString.indexOf( 'test1' ) >= 0, 'query param with null value is included' ); + assert.ok( queryString.indexOf( 'test1=' ) === -1, 'query param with null value does not generate equals sign' ); + assert.ok( queryString.indexOf( 'test2=value+%28escaped%29' ) >= 0, 'query param is url escaped' ); + + relativePath = uri.getRelativePath(); + assert.ok( relativePath.indexOf( uri.path ) >= 0, 'path in relative path' ); + assert.ok( relativePath.indexOf( uri.getQueryString() ) >= 0, 'query string in relative path' ); + assert.ok( relativePath.indexOf( uri.fragment ) >= 0, 'fragement in relative path' ); +} ); + +QUnit.test( 'Cloning', 6, function ( assert ) { + var original, clone; + + original = new mw.Uri( 'http://foo.example.org/index.php?one=1&two=2' ); + clone = original.clone(); + + assert.deepEqual( clone, original, 'clone has equivalent properties' ); + assert.equal( original.toString(), clone.toString(), 'toString matches original' ); + + assert.notStrictEqual( clone, original, 'clone is a different object when compared by reference' ); + + clone.host = 'bar.example.org'; + assert.notEqual( original.host, clone.host, 'manipulating clone did not effect original' ); + assert.notEqual( original.toString(), clone.toString(), 'Stringified url no longer matches original' ); + + clone.query.three = 3; + + assert.deepEqual( + original.query, + { 'one': '1', 'two': '2' }, + 'Properties is deep cloned (bug 37708)' + ); +} ); + +QUnit.test( 'Constructing mw.Uri from plain object', 3, function ( assert ) { + var uri = new mw.Uri({ + protocol: 'http', + host: 'www.foo.local', + path: '/this' + }); + assert.equal( uri.toString(), 'http://www.foo.local/this', 'Basic properties' ); + + uri = new mw.Uri({ + protocol: 'http', + host: 'www.foo.local', + path: '/this', + query: { hi: 'there' }, + fragment: 'blah' + }); + assert.equal( uri.toString(), 'http://www.foo.local/this?hi=there#blah', 'More complex properties' ); + + assert.throws( + function () { + var uri = new mw.Uri({ + protocol: 'http', + host: 'www.foo.local' + }); + }, + function ( e ) { + return e.message === 'Bad constructor arguments'; + }, + 'Construction failed when missing required properties' + ); +} ); + +QUnit.test( 'Manipulate properties', 8, function ( assert ) { + var uriBase, uri; + + uriBase = new mw.Uri( 'http://en.wiki.local/w/api.php' ); + + uri = uriBase.clone(); + uri.fragment = 'frag'; + assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php#frag', 'add a fragment' ); + + uri = uriBase.clone(); + uri.host = 'fr.wiki.local'; + uri.port = '8080'; + assert.equal( uri.toString(), 'http://fr.wiki.local:8080/w/api.php', 'change host and port' ); + + uri = uriBase.clone(); + uri.query.foo = 'bar'; + assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php?foo=bar', 'add query arguments' ); + + delete uri.query.foo; + assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php', 'delete query arguments' ); + + uri = uriBase.clone(); + uri.query.foo = 'bar'; + assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php?foo=bar', 'extend query arguments' ); + uri.extend({ + foo: 'quux', + pif: 'paf' + }); + assert.ok( uri.toString().indexOf( 'foo=quux' ) >= 0, 'extend query arguments' ); + assert.ok( uri.toString().indexOf( 'foo=bar' ) === -1, 'extend query arguments' ); + assert.ok( uri.toString().indexOf( 'pif=paf' ) >= 0 , 'extend query arguments' ); +} ); + +QUnit.test( 'Handle protocol-relative URLs', 5, function ( assert ) { + var UriRel, uri; + + UriRel = mw.UriRelative( 'glork://en.wiki.local/foo.php' ); + + uri = new UriRel( '//en.wiki.local/w/api.php' ); + assert.equal( uri.protocol, 'glork', 'create protocol-relative URLs with same protocol as document' ); + + uri = new UriRel( '/foo.com' ); + assert.equal( uri.toString(), 'glork://en.wiki.local/foo.com', 'handle absolute paths by supplying protocol and host from document in loose mode' ); + + uri = new UriRel( 'http:/foo.com' ); + assert.equal( uri.toString(), 'http://en.wiki.local/foo.com', 'handle absolute paths by supplying host from document in loose mode' ); + + uri = new UriRel( '/foo.com', true ); + assert.equal( uri.toString(), 'glork://en.wiki.local/foo.com', 'handle absolute paths by supplying protocol and host from document in strict mode' ); + + uri = new UriRel( 'http:/foo.com', true ); + assert.equal( uri.toString(), 'http://en.wiki.local/foo.com', 'handle absolute paths by supplying host from document in strict mode' ); +} ); + +QUnit.test( 'Bad calls', 3, function ( assert ) { + var uri; + + assert.throws( + function () { + return new mw.Uri( 'glaswegian penguins' ); + }, + function ( e ) { + return e.message === 'Bad constructor arguments'; + }, + 'throw error on non-URI as argument to constructor' + ); + + assert.throws( + function () { + return new mw.Uri( 'foo.com/bar/baz', { + strictMode: true + }); + }, + function ( e ) { + return e.message === 'Bad constructor arguments'; + }, + 'throw error on URI without protocol or // or leading / in strict mode' + ); + + uri = new mw.Uri( 'foo.com/bar/baz', { + strictMode: false + }); + assert.equal( uri.toString(), 'http://foo.com/bar/baz', 'normalize URI without protocol or // in loose mode' ); +}); + +QUnit.test( 'bug 35658', 2, function ( assert ) { + var testProtocol, testServer, testPort, testPath, UriClass, uri, href; + + testProtocol = 'https://'; + testServer = 'foo.example.org'; + testPort = '3004'; + testPath = '/!1qy'; + + UriClass = mw.UriRelative( testProtocol + testServer + '/some/path/index.html' ); + uri = new UriClass( testPath ); + href = uri.toString(); + assert.equal( href, testProtocol + testServer + testPath, 'Root-relative URL gets host & protocol supplied' ); + + UriClass = mw.UriRelative( testProtocol + testServer + ':' + testPort + '/some/path.php' ); + uri = new UriClass( testPath ); + href = uri.toString(); + assert.equal( href, testProtocol + testServer + ':' + testPort + testPath, 'Root-relative URL gets host, protocol, and port supplied' ); + +} ); + +QUnit.test( 'Constructor falls back to default location', 4, function ( assert ) { + var testuri, MyUri, uri; + + testuri = 'http://example.org/w/index.php'; + MyUri = mw.UriRelative( testuri ); + + uri = new MyUri(); + assert.equal( uri.toString(), testuri, 'no arguments' ); + + uri = new MyUri( undefined ); + assert.equal( uri.toString(), testuri, 'undefined' ); + + uri = new MyUri( null ); + assert.equal( uri.toString(), testuri, 'null' ); + + uri = new MyUri( '' ); + assert.equal( uri.toString(), testuri, 'empty string' ); +} ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js new file mode 100644 index 00000000..e2c66685 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js @@ -0,0 +1,74 @@ +QUnit.module( 'mediawiki.cldr', QUnit.newMwEnvironment() ); + +var pluralTestcases = { + /* + * Sample: + * "languagecode" : [ + * [ number, [ "form1", "form2", ... ], "expected", "description" ] + * ]; + */ + "en": [ + [ 0, [ "one", "other" ], "other", "English plural test- 0 is other" ], + [ 1, [ "one", "other" ], "one", "English plural test- 1 is one" ] + ], + "fa": [ + [ 0, [ "one", "other" ], "other", "Persian plural test- 0 is other" ], + [ 1, [ "one", "other" ], "one", "Persian plural test- 1 is one" ], + [ 2, [ "one", "other" ], "other", "Persian plural test- 2 is other" ] + ], + "fr": [ + [ 0, [ "one", "other" ], "other", "French plural test- 0 is other" ], + [ 1, [ "one", "other" ], "one", "French plural test- 1 is one" ] + ], + "hi": [ + [ 0, [ "one", "other" ], "one", "Hindi plural test- 0 is one" ], + [ 1, [ "one", "other" ], "one", "Hindi plural test- 1 is one" ], + [ 2, [ "one", "other" ], "other", "Hindi plural test- 2 is other" ] + ], + "he": [ + [ 0, [ "one", "other" ], "other", "Hebrew plural test- 0 is other" ], + [ 1, [ "one", "other" ], "one", "Hebrew plural test- 1 is one" ], + [ 2, [ "one", "other" ], "other", "Hebrew plural test- 2 is other with 2 forms" ], + [ 2, [ "one", "dual", "other" ], "dual", "Hebrew plural test- 2 is dual with 3 forms" ] + ], + "hu": [ + [ 0, [ "one", "other" ], "other", "Hungarian plural test- 0 is other" ], + [ 1, [ "one", "other" ], "one", "Hungarian plural test- 1 is one" ], + [ 2, [ "one", "other" ], "other", "Hungarian plural test- 2 is other" ] + ], + "ar": [ + [ 0, [ "zero", "one", "two", "few", "many", "other" ], "zero", "Arabic plural test - 0 is zero" ], + [ 1, [ "zero", "one", "two", "few", "many", "other" ], "one", "Arabic plural test - 1 is one" ], + [ 2, [ "zero", "one", "two", "few", "many", "other" ], "two", "Arabic plural test - 2 is two" ], + [ 3, [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 3 is few" ], + [ 9, [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 9 is few" ], + [ "9", [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 9 is few" ], + [ 110, [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 110 is few" ], + [ 11, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 11 is many" ], + [ 15, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 15 is many" ], + [ 99, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 99 is many" ], + [ 9999, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 9999 is many" ], + [ 100, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 100 is other" ], + [ 102, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 102 is other" ], + [ 1000, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 1000 is other" ], + [ 1.7, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 1.7 is other" ] + ] +}; + +function pluralTest( langCode, tests ) { + QUnit.test( 'Plural Test for ' + langCode, tests.length, function ( assert ) { + for ( var i = 0; i < tests.length; i++ ) { + assert.equal( + mw.language.convertPlural( tests[i][0], tests[i][1] ), + tests[i][2], + tests[i][3] + ); + } + } ); +} + +$.each( pluralTestcases, function ( langCode, tests ) { + if ( langCode === mw.config.get( 'wgUserLanguage' ) ) { + pluralTest( langCode, tests ); + } +} ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index 265ec2ae..b8193a92 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -1,43 +1,97 @@ -module( 'mediawiki.jqueryMsg' ); +QUnit.module( 'mediawiki.jqueryMsg' ); -test( '-- Initial check', function() { - expect( 1 ); - ok( mw.jqueryMsg, 'mw.jqueryMsg defined' ); -} ); - -test( 'mw.jqueryMsg Plural', function() { - expect( 5 ); +QUnit.test( 'mw.jqueryMsg Plural', 3, function ( assert ) { var parser = mw.jqueryMsg.getMessageFunction(); - ok( parser, 'Parser Function initialized' ); - ok( mw.messages.set( 'plural-msg', 'Found $1 {{PLURAL:$1|item|items}}' ), 'mw.messages.set: Register' ); - equal( parser( 'plural-msg', 0 ) , 'Found 0 items', 'Plural test for english with zero as count' ); - equal( parser( 'plural-msg', 1 ) , 'Found 1 item', 'Singular test for english' ); - equal( parser( 'plural-msg', 2 ) , 'Found 2 items', 'Plural test for english' ); + + mw.messages.set( 'plural-msg', 'Found $1 {{PLURAL:$1|item|items}}' ); + assert.equal( parser( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' ); + assert.equal( parser( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' ); + assert.equal( parser( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' ); } ); -test( 'mw.jqueryMsg Gender', function() { - expect( 16 ); - //TODO: These tests should be for mw.msg once mw.msg integrated with mw.jqueryMsg - var user = mw.user; +QUnit.test( 'mw.jqueryMsg Gender', 11, function ( assert ) { + // TODO: These tests should be for mw.msg once mw.msg integrated with mw.jqueryMsg + // TODO: English may not be the best language for these tests. Use a language like Arabic or Russian + var user = mw.user, + parser = mw.jqueryMsg.getMessageFunction(); + + // The values here are not significant, + // what matters is which of the values is choosen by the parser + mw.messages.set( 'gender-msg', '$1: {{GENDER:$2|blue|pink|green}}' ); + user.options.set( 'gender', 'male' ); - var parser = mw.jqueryMsg.getMessageFunction(); - ok( parser, 'Parser Function initialized' ); - //TODO: English may not be the best language for these tests. Use a language like Arabic or Russian - ok( mw.messages.set( 'gender-msg', '$1 reverted {{GENDER:$2|his|her|their}} last edit' ), 'mw.messages.set: Register' ); - equal( parser( 'gender-msg', 'Bob', 'male' ) , 'Bob reverted his last edit', 'Gender masculine' ); - equal( parser( 'gender-msg', 'Bob', user ) , 'Bob reverted his last edit', 'Gender masculine' ); + assert.equal( + parser( 'gender-msg', 'Bob', 'male' ), + 'Bob: blue', + 'Masculine from string "male"' + ); + assert.equal( + parser( 'gender-msg', 'Bob', user ), + 'Bob: blue', + 'Masculine from mw.user object' + ); + user.options.set( 'gender', 'unknown' ); - equal( parser( 'gender-msg', 'They', user ) , 'They reverted their last edit', 'Gender neutral or unknown' ); - equal( parser( 'gender-msg', 'Alice', 'female' ) , 'Alice reverted her last edit', 'Gender feminine' ); - equal( parser( 'gender-msg', 'User' ) , 'User reverted their last edit', 'Gender neutral' ); - equal( parser( 'gender-msg', 'User', 'unknown' ) , 'User reverted their last edit', 'Gender neutral' ); - ok( mw.messages.set( 'gender-msg-one-form', '{{GENDER:$1|User}} reverted last $2 {{PLURAL:$2|edit|edits}}' ), 'mw.messages.set: Register' ); - equal( parser( 'gender-msg-one-form', 'male', 10 ) , 'User reverted last 10 edits', 'Gender neutral and plural form' ); - equal( parser( 'gender-msg-one-form', 'female', 1 ) , 'User reverted last 1 edit', 'Gender neutral and singular form' ); - ok( mw.messages.set( 'gender-msg-lowercase', '{{gender:$1|he|she}} is awesome' ), 'mw.messages.set: Register' ); - equal( parser( 'gender-msg-lowercase', 'male' ) , 'he is awesome', 'Gender masculine' ); - equal( parser( 'gender-msg-lowercase', 'female' ) , 'she is awesome', 'Gender feminine' ); - ok( mw.messages.set( 'gender-msg-wrong', '{{gender}} is awesome' ), 'mw.messages.set: Register' ); - equal( parser( 'gender-msg-wrong', 'female' ) , ' is awesome', 'Wrong syntax used, but ignore the {{gender}}' ); + assert.equal( + parser( 'gender-msg', 'Foo', user ), + 'Foo: green', + 'Neutral from mw.user object' ); + assert.equal( + parser( 'gender-msg', 'Alice', 'female' ), + 'Alice: pink', + 'Feminine from string "female"' ); + assert.equal( + parser( 'gender-msg', 'User' ), + 'User: green', + 'Neutral when no parameter given' ); + assert.equal( + parser( 'gender-msg', 'User', 'unknown' ), + 'User: green', + 'Neutral from string "unknown"' + ); + + mw.messages.set( 'gender-msg-one-form', '{{GENDER:$1|User}}: $2 {{PLURAL:$2|edit|edits}}' ); + + assert.equal( + parser( 'gender-msg-one-form', 'male', 10 ), + 'User: 10 edits', + 'Gender neutral and plural form' + ); + assert.equal( + parser( 'gender-msg-one-form', 'female', 1 ), + 'User: 1 edit', + 'Gender neutral and singular form' + ); + + mw.messages.set( 'gender-msg-lowercase', '{{gender:$1|he|she}} is awesome' ); + assert.equal( + parser( 'gender-msg-lowercase', 'male' ), + 'he is awesome', + 'Gender masculine' + ); + assert.equal( + parser( 'gender-msg-lowercase', 'female' ), + 'she is awesome', + 'Gender feminine' + ); + + mw.messages.set( 'gender-msg-wrong', '{{gender}} test' ); + assert.equal( + parser( 'gender-msg-wrong', 'female' ), + ' test', + 'Invalid syntax should result in {{gender}} simply being stripped away' + ); +} ); + + +QUnit.test( 'mw.jqueryMsg Grammar', 2, function ( assert ) { + var parser = mw.jqueryMsg.getMessageFunction(); + + // Assume the grammar form grammar_case_foo is not valid in any language + mw.messages.set( 'grammar-msg', 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}' ); + assert.equal( parser( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar Test with sitename' ); + + mw.messages.set( 'grammar-msg-wrong-syntax', 'Przeszukaj {{GRAMMAR:grammar_case_xyz}}' ); + assert.equal( parser( 'grammar-msg-wrong-syntax' ), 'Przeszukaj ' , 'Grammar Test with wrong grammar template syntax' ); } ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js index 24005b64..2baa4f37 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js @@ -1,24 +1,24 @@ /* Some misc JavaScript compatibility tests, just to make sure the environments we run in are consistent */ -module( 'mediawiki.jscompat', QUnit.newMwEnvironment() ); +QUnit.module( 'mediawiki.jscompat', QUnit.newMwEnvironment() ); -test( 'Variable with Unicode letter in name', function() { - expect(3); +QUnit.test( 'Variable with Unicode letter in name', 3, function ( assert ) { var orig = "some token"; var ŝablono = orig; - deepEqual( ŝablono, orig, 'ŝablono' ); - deepEqual( \u015dablono, orig, '\\u015dablono' ); - deepEqual( \u015Dablono, orig, '\\u015Dablono' ); + + assert.deepEqual( ŝablono, orig, 'ŝablono' ); + assert.deepEqual( \u015dablono, orig, '\\u015dablono' ); + assert.deepEqual( \u015Dablono, orig, '\\u015Dablono' ); }); /* // Not that we need this. ;) // This fails on IE 6-8 // Works on IE 9, Firefox 6, Chrome 14 -test( 'Keyword workaround: "if" as variable name using Unicode escapes', function() { +QUnit.test( 'Keyword workaround: "if" as variable name using Unicode escapes', function ( assert ) { var orig = "another token"; \u0069\u0066 = orig; - deepEqual( \u0069\u0066, orig, '\\u0069\\u0066' ); + assert.deepEqual( \u0069\u0066, orig, '\\u0069\\u0066' ); }); */ @@ -26,37 +26,37 @@ test( 'Keyword workaround: "if" as variable name using Unicode escapes', functio // Not that we need this. ;) // This fails on IE 6-9 // Works on Firefox 6, Chrome 14 -test( 'Keyword workaround: "if" as member variable name using Unicode escapes', function() { +QUnit.test( 'Keyword workaround: "if" as member variable name using Unicode escapes', function ( assert ) { var orig = "another token"; var foo = {}; foo.\u0069\u0066 = orig; - deepEqual( foo.\u0069\u0066, orig, 'foo.\\u0069\\u0066' ); + assert.deepEqual( foo.\u0069\u0066, orig, 'foo.\\u0069\\u0066' ); }); */ -test( 'Stripping of single initial newline from textarea\'s literal contents (bug 12130)', function() { +QUnit.test( 'Stripping of single initial newline from textarea\'s literal contents (bug 12130)', function ( assert ) { var maxn = 4; - expect(maxn * 2); + QUnit.expect( maxn * 2 ); - var repeat = function(str, n) { - if (n <= 0) { + function repeat( str, n ) { + if ( n <= 0 ) { return ''; } else { - var out = Array(n); - for (var i = 0; i < n; i++) { + var out = new Array(n); + for ( var i = 0; i < n; i++ ) { out[i] = str; } return out.join(''); } - }; + } - for (var n = 0; n < maxn; n++) { + for ( var n = 0; n < maxn; n++ ) { var expected = repeat('\n', n) + 'some text'; var $textarea = $('<textarea>\n' + expected + '</textarea>'); - equal($textarea.val(), expected, 'Expecting ' + n + ' newlines (HTML contained ' + (n + 1) + ')'); + assert.equal( $textarea.val(), expected, 'Expecting ' + n + ' newlines (HTML contained ' + (n + 1) + ')' ); var $textarea2 = $('<textarea>').val(expected); - equal($textarea2.val(), expected, 'Expecting ' + n + ' newlines (from DOM set with ' + n + ')'); + assert.equal( $textarea2.val(), expected, 'Expecting ' + n + ' newlines (from DOM set with ' + n + ')' ); } }); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js new file mode 100644 index 00000000..3fa2b099 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js @@ -0,0 +1,394 @@ +QUnit.module( 'mediawiki.language', QUnit.newMwEnvironment({ + setup: function () { + this.liveLangData = mw.language.data.values; + mw.language.data.values = $.extend( true, {}, this.liveLangData ); + }, + teardown: function () { + // Restore + mw.language.data.values = this.liveLangData; + } +}) ); + +QUnit.test( 'mw.language getData and setData', function ( assert ) { + QUnit.expect( 2 ); + + mw.language.setData( 'en', 'testkey', 'testvalue' ); + assert.equal( mw.language.getData( 'en', 'testkey' ), 'testvalue', 'Getter setter test for mw.language' ); + assert.equal( mw.language.getData( 'en', 'invalidkey' ), undefined, 'Getter setter test for mw.language with invalid key' ); +} ); + +function grammarTest( langCode, test ) { + // The test works only if the content language is opt.language + // because it requires [lang].js to be loaded. + QUnit.test( 'Grammar test for lang=' + langCode, function ( assert ) { + QUnit.expect( test.length ); + + for ( var i = 0 ; i < test.length; i++ ) { + assert.equal( + mw.language.convertGrammar( test[i].word, test[i].grammarForm ), + test[i].expected, + test[i].description + ); + } + }); +} + +var grammarTests = { + bs: [ + { + word: 'word', + grammarForm: 'instrumental', + expected: 's word', + description: 'Grammar test for instrumental case' + }, + { + word: 'word', + grammarForm: 'lokativ', + expected: 'o word', + description: 'Grammar test for lokativ case' + } + ], + + he: [ + { + word: "ויקיפדיה", + grammarForm: 'prefixed', + expected: "וויקיפדיה", + description: 'Duplicate the "Waw" if prefixed' + }, + { + word: "וולפגנג", + grammarForm: 'prefixed', + expected: "וולפגנג", + description: 'Duplicate the "Waw" if prefixed, but not if it is already duplicated.' + }, + { + word: "הקובץ", + grammarForm: 'prefixed', + expected: "קובץ", + description: 'Remove the "He" if prefixed' + }, + { + word: 'Wikipedia', + grammarForm: 'תחילית', + expected: '־Wikipedia', + description: 'GAdd a hyphen (maqaf) before non-Hebrew letters' + }, + { + word: '1995', + grammarForm: 'תחילית', + expected: '־1995', + description: 'Add a hyphen (maqaf) before numbers' + } + ], + + hsb: [ + { + word: 'word', + grammarForm: 'instrumental', + expected: 'z word', + description: 'Grammar test for instrumental case' + }, + { + word: 'word', + grammarForm: 'lokatiw', + expected: 'wo word', + description: 'Grammar test for lokatiw case' + } + ], + + dsb: [ + { + word: 'word', + grammarForm: 'instrumental', + expected: 'z word', + description: 'Grammar test for instrumental case' + }, + { + word: 'word', + grammarForm: 'lokatiw', + expected: 'wo word', + description: 'Grammar test for lokatiw case' + } + ], + + hy: [ + { + word: 'Մաունա', + grammarForm: 'genitive', + expected: 'Մաունայի', + description: 'Grammar test for genitive case' + }, + { + word: 'հետո', + grammarForm: 'genitive', + expected: 'հետոյի', + description: 'Grammar test for genitive case' + }, + { + word: 'գիրք', + grammarForm: 'genitive', + expected: 'գրքի', + description: 'Grammar test for genitive case' + }, + { + word: 'ժամանակի', + grammarForm: 'genitive', + expected: 'ժամանակիի', + description: 'Grammar test for genitive case' + } + ], + + fi: [ + { + word: 'talo', + grammarForm: 'genitive', + expected: 'talon', + description: 'Grammar test for genitive case' + }, + { + word: 'linux', + grammarForm: 'genitive', + expected: 'linuxin', + description: 'Grammar test for genitive case' + }, + { + word: 'talo', + grammarForm: 'elative', + expected: 'talosta', + description: 'Grammar test for elative case' + }, + { + word: 'pastöroitu', + grammarForm: 'partitive', + expected: 'pastöroitua', + description: 'Grammar test for partitive case' + }, + { + word: 'talo', + grammarForm: 'partitive', + expected: 'taloa', + description: 'Grammar test for partitive case' + }, + { + word: 'talo', + grammarForm: 'illative', + expected: 'taloon', + description: 'Grammar test for illative case' + }, + { + word: 'linux', + grammarForm: 'inessive', + expected: 'linuxissa', + description: 'Grammar test for inessive case' + } + ], + + ru: [ + { + word: 'тесть', + grammarForm: 'genitive', + expected: 'тестя', + description: 'Grammar test for genitive case' + }, + { + word: 'привилегия', + grammarForm: 'genitive', + expected: 'привилегии', + description: 'Grammar test for genitive case' + }, + { + word: 'установка', + grammarForm: 'genitive', + expected: 'установки', + description: 'Grammar test for genitive case' + }, + { + word: 'похоти', + grammarForm: 'genitive', + expected: 'похотей', + description: 'Grammar test for genitive case' + }, + { + word: 'доводы', + grammarForm: 'genitive', + expected: 'доводов', + description: 'Grammar test for genitive case' + }, + { + word: 'песчаник', + grammarForm: 'genitive', + expected: 'песчаника', + description: 'Grammar test for genitive case' + } + ], + + + hu: [ + { + word: 'Wikipédiá', + grammarForm: 'rol', + expected: 'Wikipédiáról', + description: 'Grammar test for rol case' + }, + { + word: 'Wikipédiá', + grammarForm: 'ba', + expected: 'Wikipédiába', + description: 'Grammar test for ba case' + }, + { + word: 'Wikipédiá', + grammarForm: 'k', + expected: 'Wikipédiák', + description: 'Grammar test for k case' + } + ], + + ga: [ + { + word: 'an Domhnach', + grammarForm: 'ainmlae', + expected: 'Dé Domhnaigh', + description: 'Grammar test for ainmlae case' + }, + { + word: 'an Luan', + grammarForm: 'ainmlae', + expected: 'Dé Luain', + description: 'Grammar test for ainmlae case' + }, + { + word: 'an Satharn', + grammarForm: 'ainmlae', + expected: 'Dé Sathairn', + description: 'Grammar test for ainmlae case' + } + ], + + uk: [ + { + word: 'тесть', + grammarForm: 'genitive', + expected: 'тестя', + description: 'Grammar test for genitive case' + }, + { + word: 'Вікіпедія', + grammarForm: 'genitive', + expected: 'Вікіпедії', + description: 'Grammar test for genitive case' + }, + { + word: 'установка', + grammarForm: 'genitive', + expected: 'установки', + description: 'Grammar test for genitive case' + }, + { + word: 'похоти', + grammarForm: 'genitive', + expected: 'похотей', + description: 'Grammar test for genitive case' + }, + { + word: 'доводы', + grammarForm: 'genitive', + expected: 'доводов', + description: 'Grammar test for genitive case' + }, + { + word: 'песчаник', + grammarForm: 'genitive', + expected: 'песчаника', + description: 'Grammar test for genitive case' + }, + { + word: 'Вікіпедія', + grammarForm: 'accusative', + expected: 'Вікіпедію', + description: 'Grammar test for accusative case' + } + ], + + sl: [ + { + word: 'word', + grammarForm: 'orodnik', + expected: 'z word', + description: 'Grammar test for orodnik case' + }, + { + word: 'word', + grammarForm: 'mestnik', + expected: 'o word', + description: 'Grammar test for mestnik case' + } + ], + + os: [ + { + word: 'бæстæ', + grammarForm: 'genitive', + expected: 'бæсты', + description: 'Grammar test for genitive case' + }, + { + word: 'бæстæ', + grammarForm: 'allative', + expected: 'бæстæм', + description: 'Grammar test for allative case' + }, + { + word: 'Тигр', + grammarForm: 'dative', + expected: 'Тигрæн', + description: 'Grammar test for dative case' + }, + { + word: 'цъити', + grammarForm: 'dative', + expected: 'цъитийæн', + description: 'Grammar test for dative case' + }, + { + word: 'лæппу', + grammarForm: 'genitive', + expected: 'лæппуйы', + description: 'Grammar test for genitive case' + }, + { + word: '2011', + grammarForm: 'equative', + expected: '2011-ау', + description: 'Grammar test for equative case' + } + ], + + la: [ + { + word: 'Translatio', + grammarForm: 'genitive', + expected: 'Translationis', + description: 'Grammar test for genitive case' + }, + { + word: 'Translatio', + grammarForm: 'accusative', + expected: 'Translationem', + description: 'Grammar test for accusative case' + }, + { + word: 'Translatio', + grammarForm: 'ablative', + expected: 'Translatione', + description: 'Grammar test for ablative case' + } + ] +}; + +$.each( grammarTests, function ( langCode, test ) { + if ( langCode === mw.config.get( 'wgUserLanguage' ) ) { + grammarTest( langCode, test ); + } +}); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js index e6934eda..be59f1ad 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js @@ -1,71 +1,71 @@ -module( 'mediawiki', QUnit.newMwEnvironment() ); +( function ( mw ) { -test( '-- Initial check', function() { - expect(8); +QUnit.module( 'mediawiki', QUnit.newMwEnvironment() ); - ok( window.jQuery, 'jQuery defined' ); - ok( window.$, '$j defined' ); - ok( window.$j, '$j defined' ); - strictEqual( window.$, window.jQuery, '$ alias to jQuery' ); - strictEqual( window.$j, window.jQuery, '$j alias to jQuery' ); +QUnit.test( 'Initial check', 8, function ( assert ) { + assert.ok( window.jQuery, 'jQuery defined' ); + assert.ok( window.$, '$j defined' ); + assert.ok( window.$j, '$j defined' ); + assert.strictEqual( window.$, window.jQuery, '$ alias to jQuery' ); + assert.strictEqual( window.$j, window.jQuery, '$j alias to jQuery' ); - ok( window.mediaWiki, 'mediaWiki defined' ); - ok( window.mw, 'mw defined' ); - strictEqual( window.mw, window.mediaWiki, 'mw alias to mediaWiki' ); + assert.ok( window.mediaWiki, 'mediaWiki defined' ); + assert.ok( window.mw, 'mw defined' ); + assert.strictEqual( window.mw, window.mediaWiki, 'mw alias to mediaWiki' ); }); -test( 'mw.Map', function() { - expect(17); +QUnit.test( 'mw.Map', 17, function ( assert ) { + var arry, conf, funky, globalConf, nummy, someValues; - ok( mw.Map, 'mw.Map defined' ); + assert.ok( mw.Map, 'mw.Map defined' ); - var conf = new mw.Map(), - // Dummy variables - funky = function() {}, - arry = [], - nummy = 7; + conf = new mw.Map(); + // Dummy variables + funky = function () {}; + arry = []; + nummy = 7; // Tests for input validation - strictEqual( conf.get( 'inexistantKey' ), null, 'Map.get returns null if selection was a string and the key was not found' ); - strictEqual( conf.set( 'myKey', 'myValue' ), true, 'Map.set returns boolean true if a value was set for a valid key string' ); - strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' ); - strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' ); - strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' ); - equal( conf.get( 'myKey' ), 'myValue', 'Map.get returns a single value value correctly' ); - strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' ); - strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' ); + assert.strictEqual( conf.get( 'inexistantKey' ), null, 'Map.get returns null if selection was a string and the key was not found' ); + assert.strictEqual( conf.set( 'myKey', 'myValue' ), true, 'Map.set returns boolean true if a value was set for a valid key string' ); + assert.strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' ); + assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' ); + assert.strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' ); + assert.equal( conf.get( 'myKey' ), 'myValue', 'Map.get returns a single value value correctly' ); + assert.strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' ); + assert.strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' ); // Multiple values at once - var someValues = { + someValues = { 'foo': 'bar', 'lorem': 'ipsum', 'MediaWiki': true }; - strictEqual( conf.set( someValues ), true, 'Map.set returns boolean true if multiple values were set by passing an object' ); - deepEqual( conf.get( ['foo', 'lorem'] ), { + assert.strictEqual( conf.set( someValues ), true, 'Map.set returns boolean true if multiple values were set by passing an object' ); + assert.deepEqual( conf.get( ['foo', 'lorem'] ), { 'foo': 'bar', 'lorem': 'ipsum' }, 'Map.get returns multiple values correctly as an object' ); - deepEqual( conf.get( ['foo', 'notExist'] ), { + assert.deepEqual( conf.get( ['foo', 'notExist'] ), { 'foo': 'bar', 'notExist': null }, 'Map.get return includes keys that were not found as null values' ); - strictEqual( conf.exists( 'foo' ), true, 'Map.exists returns boolean true if a key exists' ); - strictEqual( conf.exists( 'notExist' ), false, 'Map.exists returns boolean false if a key does not exists' ); + assert.strictEqual( conf.exists( 'foo' ), true, 'Map.exists returns boolean true if a key exists' ); + assert.strictEqual( conf.exists( 'notExist' ), false, 'Map.exists returns boolean false if a key does not exists' ); // Interacting with globals and accessing the values object - strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' ); + assert.strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' ); conf.set( 'globalMapChecker', 'Hi' ); - ok( false === 'globalMapChecker' in window, 'new mw.Map did not store its values in the global window object by default' ); + assert.ok( false === 'globalMapChecker' in window, 'new mw.Map did not store its values in the global window object by default' ); - var globalConf = new mw.Map( true ); + globalConf = new mw.Map( true ); globalConf.set( 'anotherGlobalMapChecker', 'Hello' ); - ok( 'anotherGlobalMapChecker' in window, 'new mw.Map( true ) did store its values in the global window object' ); + assert.ok( 'anotherGlobalMapChecker' in window, 'new mw.Map( true ) did store its values in the global window object' ); // Whitelist this global variable for QUnit's 'noglobal' mode if ( QUnit.config.noglobals ) { @@ -73,124 +73,488 @@ test( 'mw.Map', function() { } }); -test( 'mw.config', function() { - expect(1); - - ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' ); +QUnit.test( 'mw.config', 1, function ( assert ) { + assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' ); }); -test( 'mw.message & mw.messages', function() { - expect(20); +QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) { + var goodbye, hello, pluralMessage; - ok( mw.messages, 'messages defined' ); - ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' ); - ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' ); + assert.ok( mw.messages, 'messages defined' ); + assert.ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' ); + assert.ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' ); - var hello = mw.message( 'hello' ); + hello = mw.message( 'hello' ); - equal( hello.format, 'plain', 'Message property "format" defaults to "plain"' ); - strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' ); - equal( hello.key, 'hello', 'Message property "key" (currect key)' ); - deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' ); + assert.equal( hello.format, 'plain', 'Message property "format" defaults to "plain"' ); + assert.strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' ); + assert.equal( hello.key, 'hello', 'Message property "key" (currect key)' ); + assert.deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' ); // Todo - ok( hello.params, 'Message prototype "params"' ); + assert.ok( hello.params, 'Message prototype "params"' ); hello.format = 'plain'; - equal( hello.toString(), 'Hello <b>awesome</b> world', 'Message.toString returns the message as a string with the current "format"' ); + assert.equal( hello.toString(), 'Hello <b>awesome</b> world', 'Message.toString returns the message as a string with the current "format"' ); - equal( hello.escaped(), 'Hello <b>awesome</b> world', 'Message.escaped returns the escaped message' ); - equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' ); + assert.equal( hello.escaped(), 'Hello <b>awesome</b> world', 'Message.escaped returns the escaped message' ); + assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' ); hello.parse(); - equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' ); + assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' ); hello.plain(); - equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' ); + assert.equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' ); - strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' ); + assert.strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' ); - var goodbye = mw.message( 'goodbye' ); - strictEqual( goodbye.exists(), false, 'Message.exists returns false for nonexistent messages' ); + goodbye = mw.message( 'goodbye' ); + assert.strictEqual( goodbye.exists(), false, 'Message.exists returns false for nonexistent messages' ); - equal( goodbye.plain(), '<goodbye>', 'Message.toString returns plain <key> if format is "plain" and key does not exist' ); + assert.equal( goodbye.plain(), '<goodbye>', 'Message.toString returns plain <key> if format is "plain" and key does not exist' ); // bug 30684 - equal( goodbye.escaped(), '<goodbye>', 'Message.toString returns properly escaped <key> if format is "escaped" and key does not exist' ); + assert.equal( goodbye.escaped(), '<goodbye>', 'Message.toString returns properly escaped <key> if format is "escaped" and key does not exist' ); - ok( mw.messages.set( 'pluraltestmsg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' ); - var pluralMessage = mw.message( 'pluraltestmsg' , 6 ); - equal( pluralMessage.plain(), 'There are 6 results', 'plural get resolved when format is plain' ); - equal( pluralMessage.parse(), 'There are 6 results', 'plural get resolved when format is parse' ); + assert.ok( mw.messages.set( 'pluraltestmsg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' ); + pluralMessage = mw.message( 'pluraltestmsg' , 6 ); + assert.equal( pluralMessage.plain(), 'There are 6 results', 'plural get resolved when format is plain' ); + assert.equal( pluralMessage.parse(), 'There are 6 results', 'plural get resolved when format is parse' ); }); -test( 'mw.msg', function() { - expect(11); - - ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' ); - equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' ); - equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' ); +QUnit.test( 'mw.msg', 11, function ( assert ) { + assert.ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' ); + assert.equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' ); + assert.equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' ); - ok( mw.messages.set( 'plural-item' , 'Found $1 {{PLURAL:$1|item|items}}' ) ); - equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' ); - equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' ); - equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' ); + assert.ok( mw.messages.set( 'plural-item' , 'Found $1 {{PLURAL:$1|item|items}}' ) ); + assert.equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' ); + assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' ); + assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' ); - ok( mw.messages.set('gender-plural-msg' , '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome' ) ); - equal( mw.msg( 'gender-plural-msg', 'male', 1 ), 'he is awesome', 'Gender test for male, plural count 1' ); - equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' ); - equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' ); + assert.ok( mw.messages.set('gender-plural-msg' , '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome' ) ); + assert.equal( mw.msg( 'gender-plural-msg', 'male', 1 ), 'he is awesome', 'Gender test for male, plural count 1' ); + assert.equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' ); + assert.equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' ); }); -test( 'mw.loader', function() { - expect(1); +/** + * The sync style load test (for @import). This is, in a way, also an open bug for + * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a + * way to get a callback from when a stylesheet is loaded (that is, including any + * @import rules inside). To work around this, we'll have a little time loop to check + * if the styles apply. + * Note: This test originally used new Image() and onerror to get a callback + * when the url is loaded, but that is fragile since it doesn't monitor the + * same request as the css @import, and Safari 4 has issues with + * onerror/onload not being fired at all in weird cases like this. + */ +function assertStyleAsync( assert, $element, prop, val, fn ) { + var styleTestStart, + el = $element.get( 0 ), + styleTestTimeout = ( QUnit.config.testTimeout - 200 ) || 5000; + + function isCssImportApplied() { + // Trigger reflow, repaint, redraw, whatever (cross-browser) + var x = $element.css( 'height' ); + x = el.innerHTML; + el.className = el.className; + x = document.documentElement.clientHeight; + + return $element.css( prop ) === val; + } + + function styleTestLoop() { + var styleTestSince = new Date().getTime() - styleTestStart; + // If it is passing or if we timed out, run the real test and stop the loop + if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) { + assert.equal( $element.css( prop ), val, + 'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)' + ); + + if ( fn ) { + fn(); + } + + return; + } + // Otherwise, keep polling + setTimeout( styleTestLoop, 150 ); + } + + // Start the loop + styleTestStart = new Date().getTime(); + styleTestLoop(); +} + +function urlStyleTest( selector, prop, val ) { + return QUnit.fixurl( + mw.config.get( 'wgScriptPath' ) + + '/tests/qunit/data/styleTest.css.php?' + + $.param( { + selector: selector, + prop: prop, + val: val + } ) + ); +} + +QUnit.asyncTest( 'mw.loader', 2, function ( assert ) { + var isAwesomeDone; - // Asynchronous ahead - stop(); + mw.loader.testCallback = function () { + QUnit.start(); + assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined'); + isAwesomeDone = true; + }; - mw.loader.implement( 'is.awesome', [QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/defineTestCallback.js' )], {}, {} ); + mw.loader.implement( 'test.callback', [QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' )], {}, {} ); - mw.loader.using( 'is.awesome', function() { + mw.loader.using( 'test.callback', function () { // /sample/awesome.js declares the "mw.loader.testCallback" function // which contains a call to start() and ok() - mw.loader.testCallback(); - mw.loader.testCallback = undefined; + assert.strictEqual( isAwesomeDone, true, "test.callback module should've caused isAwesomeDone to be true" ); + delete mw.loader.testCallback; - }, function() { - start(); - ok( false, 'Error callback fired while implementing "is.awesome" module' ); + }, function () { + QUnit.start(); + assert.ok( false, 'Error callback fired while loader.using "test.callback" module' ); }); - }); -test( 'mw.loader.bug29107' , function() { - expect(2); +QUnit.test( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) { + var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' ); + + assert.notEqual( + $element.css( 'float' ), + 'right', + 'style is clear' + ); + + mw.loader.implement( + 'test.implement.a', + function () { + assert.equal( + $element.css( 'float' ), + 'right', + 'style is applied' + ); + }, + { + 'all': '.mw-test-implement-a { float: right; }' + }, + {} + ); + + mw.loader.load([ + 'test.implement.a' + ]); +} ); + +QUnit.asyncTest( 'mw.loader.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) { + var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ), + $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ), + $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ); + + assert.notEqual( + $element1.css( 'text-align' ), + 'center', + 'style is clear' + ); + assert.notEqual( + $element2.css( 'float' ), + 'left', + 'style is clear' + ); + assert.notEqual( + $element3.css( 'text-align' ), + 'right', + 'style is clear' + ); + + mw.loader.implement( + 'test.implement.b', + function () { + assertStyleAsync( assert, $element2, 'float', 'left', function () { + assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' ); + + QUnit.start(); + } ); + assertStyleAsync( assert, $element3, 'float', 'right', function () { + assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' ); + + QUnit.start(); + } ); + }, + { + 'url': { + 'print': [urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' )], + 'screen': [ + // bug 40834: Make sure it actually works with more than 1 stylesheet reference + urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ), + urlStyleTest( '.mw-test-implement-b3', 'float', 'right' ) + ] + } + }, + {} + ); + + mw.loader.load([ + 'test.implement.b' + ]); +} ); + +// Backwards compatibility +QUnit.test( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) { + var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' ); + + assert.notEqual( + $element.css( 'float' ), + 'right', + 'style is clear' + ); + + mw.loader.implement( + 'test.implement.c', + function () { + assert.equal( + $element.css( 'float' ), + 'right', + 'style is applied' + ); + }, + { + 'all': '.mw-test-implement-c { float: right; }' + }, + {} + ); + + mw.loader.load([ + 'test.implement.c' + ]); +} ); + +// Backwards compatibility +QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) { + var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ), + $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ); + + assert.notEqual( + $element.css( 'float' ), + 'right', + 'style is clear' + ); + assert.notEqual( + $element2.css( 'text-align' ), + 'center', + 'style is clear' + ); + + mw.loader.implement( + 'test.implement.d', + function () { + assertStyleAsync( assert, $element, 'float', 'right', function () { + + assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' ); + + QUnit.start(); + } ); + }, + { + 'all': [urlStyleTest( '.mw-test-implement-d', 'float', 'right' )], + 'print': [urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' )] + }, + {} + ); - // Message doesn't exist already - ok( !mw.messages.exists( 'bug29107' ) ); + mw.loader.load([ + 'test.implement.d' + ]); +} ); + +// @import (bug 31676) +QUnit.asyncTest( 'mw.loader.implement( styles has @import)', 5, function ( assert ) { + var isJsExecuted, $element; + + mw.loader.implement( + 'test.implement.import', + function () { + assert.strictEqual( isJsExecuted, undefined, 'javascript not executed multiple times' ); + isJsExecuted = true; + + assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state is "ready" while implement() is executing javascript' ); + + $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' ); + + assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'Messages are loaded before javascript execution' ); + + assertStyleAsync( assert, $element, 'float', 'right', function () { + assert.equal( $element.css( 'text-align' ),'center', + 'CSS styles after the @import rule are working' + ); + + QUnit.start(); + } ); + }, + { + 'css': [ + '@import url(\'' + + urlStyleTest( '.mw-test-implement-import', 'float', 'right' ) + + '\');\n' + + '.mw-test-implement-import { text-align: center; }' + ] + }, + { + 'test-foobar': 'Hello Foobar, $1!' + } + ); + + mw.loader.load( 'test.implement' ); + +}); - // Async! Failure in this test may lead to neither the success nor error callbacks getting called. - // Due to QUnit's timeout feauture we won't hang here forever if this happends. - stop(); +QUnit.asyncTest( 'mw.loader.implement( only messages )' , 2, function ( assert ) { + assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' ); - mw.loader.implement( 'bug29107.messages-only', [], {}, {'bug29107': 'loaded'} ); - mw.loader.using( 'bug29107.messages-only', function() { - start(); - ok( mw.messages.exists( 'bug29107' ), 'Bug 29107: messages-only module should implement ok' ); + mw.loader.implement( 'test.implement.msgs', [], {}, { 'bug_29107': 'loaded' } ); + mw.loader.using( 'test.implement.msgs', function() { + QUnit.start(); + assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' ); }, function() { - start(); - ok( false, 'Error callback fired while implementing "bug29107.messages-only" module' ); + QUnit.start(); + assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' ); }); }); -test( 'mw.loader.bug30825', function() { +QUnit.test( 'mw.loader erroneous indirect dependency', 3, function ( assert ) { + mw.loader.register( [ + ['test.module1', '0'], + ['test.module2', '0', ['test.module1']], + ['test.module3', '0', ['test.module2']] + ] ); + mw.loader.implement( 'test.module1', function () { throw new Error( 'expected' ); }, {}, {} ); + assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' ); + assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' ); + assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' ); +} ); + +QUnit.test( 'mw.loader out-of-order implementation', 9, function ( assert ) { + mw.loader.register( [ + ['test.module4', '0'], + ['test.module5', '0', ['test.module4']], + ['test.module6', '0', ['test.module5']] + ] ); + mw.loader.implement( 'test.module4', function () {}, {}, {} ); + assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' ); + assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' ); + assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' ); + mw.loader.implement( 'test.module6', function () {}, {}, {} ); + assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' ); + assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' ); + assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' ); + mw.loader.implement( 'test.module5', function() {}, {}, {} ); + assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' ); + assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' ); + assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' ); +} ); + +QUnit.test( 'mw.loader missing dependency', 13, function ( assert ) { + mw.loader.register( [ + ['test.module7', '0'], + ['test.module8', '0', ['test.module7']], + ['test.module9', '0', ['test.module8']] + ] ); + mw.loader.implement( 'test.module8', function () {}, {}, {} ); + assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' ); + assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' ); + assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' ); + mw.loader.state( 'test.module7', 'missing' ); + assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' ); + assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' ); + assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' ); + mw.loader.implement( 'test.module9', function () {}, {}, {} ); + assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' ); + assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' ); + assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' ); + mw.loader.using( + ['test.module7'], + function () { + assert.ok( false, "Success fired despite missing dependency" ); + assert.ok( true , "QUnit expected() count dummy" ); + }, + function ( e, dependencies ) { + assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' ); + assert.deepEqual( dependencies, ['test.module7'], 'Error callback called with module test.module7' ); + } + ); + mw.loader.using( + ['test.module9'], + function () { + assert.ok( false, "Success fired despite missing dependency" ); + assert.ok( true , "QUnit expected() count dummy" ); + }, + function ( e, dependencies ) { + assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' ); + dependencies.sort(); + assert.deepEqual( + dependencies, + ['test.module7', 'test.module8', 'test.module9'], + 'Error callback called with all three modules as dependencies' + ); + } + ); +} ); + +QUnit.asyncTest( 'mw.loader dependency handling', 5, function ( assert ) { + mw.loader.addSource( + 'testloader', + { + loadScript: QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' ) + } + ); + + mw.loader.register( [ + // [module, version, dependencies, group, source] + ['testMissing', '1', [], null, 'testloader'], + ['testUsesMissing', '1', ['testMissing'], null, 'testloader'], + ['testUsesNestedMissing', '1', ['testUsesMissing'], null, 'testloader'] + ] ); + + function verifyModuleStates() { + assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' ); + assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' ); + assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' ); + } + + mw.loader.using( ['testUsesNestedMissing'], + function () { + assert.ok( false, 'Error handler should be invoked.' ); + assert.ok( true ); // Dummy to reach QUnit expect() + + verifyModuleStates(); + + QUnit.start(); + }, + function ( e, badmodules ) { + assert.ok( true, 'Error handler should be invoked.' ); + // As soon as server spits out state('testMissing', 'missing'); + // it will bubble up and trigger the error callback. + // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing. + assert.deepEqual( badmodules, ['testMissing'], 'Bad modules as expected.' ); + + verifyModuleStates(); + + QUnit.start(); + } + ); +} ); + +QUnit.asyncTest( 'mw.loader( "//protocol-relative" ) (bug 30825)', 2, function ( assert ) { // This bug was actually already fixed in 1.18 and later when discovered in 1.17. // Test is for regressions! - expect(2); - // Forge an URL to the test callback script var target = QUnit.fixurl( mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js' @@ -199,31 +563,31 @@ test( 'mw.loader.bug30825', function() { // Confirm that mw.loader.load() works with protocol-relative URLs target = target.replace( /https?:/, '' ); - equal( target.substr( 0, 2 ), '//', + assert.equal( target.substr( 0, 2 ), '//', 'URL must be relative to test relative URLs!' ); // Async! - stop(); + // The target calls QUnit.start mw.loader.load( target ); }); -test( 'mw.html', function() { - expect(11); - - raises( function(){ +QUnit.test( 'mw.html', 13, function ( assert ) { + assert.throws( function () { mw.html.escape(); }, TypeError, 'html.escape throws a TypeError if argument given is not a string' ); - equal( mw.html.escape( '<mw awesome="awesome" value=\'test\' />' ), - '<mw awesome="awesome" value='test' />', 'html.escape escapes html snippet' ); + assert.equal( mw.html.escape( '<mw awesome="awesome" value=\'test\' />' ), + '<mw awesome="awesome" value='test' />', 'escape() escapes special characters to html entities' ); - equal( mw.html.element(), - '<undefined/>', 'html.element Always return a valid html string (even without arguments)' ); + assert.equal( mw.html.element(), + '<undefined/>', 'element() always returns a valid html string (even without arguments)' ); - equal( mw.html.element( 'div' ), '<div/>', 'html.element DIV (simple)' ); + assert.equal( mw.html.element( 'div' ), '<div/>', 'element() Plain DIV (simple)' ); - equal( + assert.equal( mw.html.element( 'div', {}, '' ), '<div></div>', 'element() Basic DIV (simple)' ); + + assert.equal( mw.html.element( 'div', { id: 'foobar' @@ -232,11 +596,24 @@ test( 'mw.html', function() { '<div id="foobar"/>', 'html.element DIV (attribs)' ); - equal( mw.html.element( 'p', null, 12 ), '<p>12</p>', 'Numbers are valid content and should be casted to a string' ); + assert.equal( mw.html.element( 'p', null, 12 ), '<p>12</p>', 'Numbers are valid content and should be casted to a string' ); + + assert.equal( mw.html.element( 'p', { title: 12 }, '' ), '<p title="12"></p>', 'Numbers are valid attribute values' ); - equal( mw.html.element( 'p', { title: 12 }, '' ), '<p title="12"></p>', 'Numbers are valid attribute values' ); + // Example from https://www.mediawiki.org/wiki/ResourceLoader/Default_modules#mediaWiki.html + assert.equal( + mw.html.element( + 'div', + {}, + new mw.html.Raw( + mw.html.element( 'img', { src: '<' } ) + ) + ), + '<div><img src="<"/></div>', + 'Raw inclusion of another element' + ); - equal( + assert.equal( mw.html.element( 'option', { selected: true @@ -246,7 +623,7 @@ test( 'mw.html', function() { 'Attributes may have boolean values. True copies the attribute name to the value.' ); - equal( + assert.equal( mw.html.element( 'option', { value: 'foo', @@ -257,14 +634,16 @@ test( 'mw.html', function() { 'Attributes may have boolean values. False keeps the attribute from output.' ); - equal( mw.html.element( 'div', + assert.equal( mw.html.element( 'div', null, 'a' ), '<div>a</div>', 'html.element DIV (content)' ); - equal( mw.html.element( 'a', + assert.equal( mw.html.element( 'a', { href: 'http://mediawiki.org/w/index.php?title=RL&action=history' }, 'a' ), '<a href="http://mediawiki.org/w/index.php?title=RL&action=history">a</a>', 'html.element DIV (attribs + content)' ); }); + +}( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js index 15265db5..16c97dff 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js @@ -1,21 +1,12 @@ -module( 'mediawiki.user', QUnit.newMwEnvironment() ); +( function ( mw ) { -test( '-- Initial check', function() { - expect(1); +QUnit.module( 'mediawiki.user', QUnit.newMwEnvironment() ); - ok( mw.user, 'mw.user defined' ); +QUnit.test( 'options', 1, function ( assert ) { + assert.ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' ); }); - -test( 'options', function() { - expect(1); - - ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' ); -}); - -test( 'User login status', function() { - expect(5); - +QUnit.test( 'user status', 9, function ( assert ) { /** * Tests can be run under three different conditions: * 1) From tests/qunit/index.html, user will be anonymous. @@ -24,16 +15,42 @@ test( 'User login status', function() { */ // Forge an anonymous user: - mw.config.set( 'wgUserName', null); + mw.config.set( 'wgUserName', null ); - strictEqual( mw.user.name(), null, 'user.name should return null when anonymous' ); - ok( mw.user.anonymous(), 'user.anonymous should reutrn true when anonymous' ); + assert.strictEqual( mw.user.getName(), null, 'user.getName() returns null when anonymous' ); + assert.strictEqual( mw.user.name(), null, 'user.name() compatibility' ); + assert.assertTrue( mw.user.isAnon(), 'user.isAnon() returns true when anonymous' ); + assert.assertTrue( mw.user.anonymous(), 'user.anonymous() compatibility' ); // Not part of startUp module mw.config.set( 'wgUserName', 'John' ); - equal( mw.user.name(), 'John', 'user.name returns username when logged-in' ); - ok( !mw.user.anonymous(), 'user.anonymous returns false when logged-in' ); + assert.equal( mw.user.getName(), 'John', 'user.getName() returns username when logged-in' ); + assert.equal( mw.user.name(), 'John', 'user.name() compatibility' ); + assert.assertFalse( mw.user.isAnon(), 'user.isAnon() returns false when logged-in' ); + assert.assertFalse( mw.user.anonymous(), 'user.anonymous() compatibility' ); - equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' ); + assert.equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' ); }); + +QUnit.asyncTest( 'getGroups', 3, function ( assert ) { + mw.user.getGroups( function ( groups ) { + // First group should always be '*' + assert.equal( $.type( groups ), 'array', 'Callback gets an array' ); + assert.notStrictEqual( $.inArray( '*', groups ), -1, '"*"" is in the list' ); + // Sort needed because of different methods if creating the arrays, + // only the content matters. + assert.deepEqual( groups.sort(), mw.config.get( 'wgUserGroups' ).sort(), 'Array contains all groups, just like wgUserGroups' ); + QUnit.start(); + }); +}); + +QUnit.asyncTest( 'getRights', 1, function ( assert ) { + mw.user.getRights( function ( rights ) { + // First group should always be '*' + assert.equal( $.type( rights ), 'array', 'Callback gets an array' ); + QUnit.start(); + }); +}); + +}( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js index ea28935e..ababa8d9 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js @@ -1,73 +1,58 @@ -module( 'mediawiki.util', QUnit.newMwEnvironment() ); +QUnit.module( 'mediawiki.util', QUnit.newMwEnvironment() ); -test( '-- Initial check', function() { - expect(1); - - ok( mw.util, 'mw.util defined' ); -}); - -test( 'rawurlencode', function() { - expect(1); - - equal( mw.util.rawurlencode( 'Test:A & B/Here' ), 'Test%3AA%20%26%20B%2FHere' ); +QUnit.test( 'rawurlencode', 1, function ( assert ) { + assert.equal( mw.util.rawurlencode( 'Test:A & B/Here' ), 'Test%3AA%20%26%20B%2FHere' ); }); -test( 'wikiUrlencode', function() { - expect(1); - - equal( mw.util.wikiUrlencode( 'Test:A & B/Here' ), 'Test:A_%26_B/Here' ); +QUnit.test( 'wikiUrlencode', 1, function ( assert ) { + assert.equal( mw.util.wikiUrlencode( 'Test:A & B/Here' ), 'Test:A_%26_B/Here' ); }); -test( 'wikiGetlink', function() { - expect(3); - +QUnit.test( 'wikiGetlink', 3, function ( assert ) { // Not part of startUp module mw.config.set( 'wgArticlePath', '/wiki/$1' ); mw.config.set( 'wgPageName', 'Foobar' ); var hrefA = mw.util.wikiGetlink( 'Sandbox' ); - equal( hrefA, '/wiki/Sandbox', 'Simple title; Get link for "Sandbox"' ); + assert.equal( hrefA, '/wiki/Sandbox', 'Simple title; Get link for "Sandbox"' ); var hrefB = mw.util.wikiGetlink( 'Foo:Sandbox ? 5+5=10 ! (test)/subpage' ); - equal( hrefB, '/wiki/Foo:Sandbox_%3F_5%2B5%3D10_%21_%28test%29/subpage', + assert.equal( hrefB, '/wiki/Foo:Sandbox_%3F_5%2B5%3D10_%21_%28test%29/subpage', 'Advanced title; Get link for "Foo:Sandbox ? 5+5=10 ! (test)/subpage"' ); var hrefC = mw.util.wikiGetlink(); - equal( hrefC, '/wiki/Foobar', 'Default title; Get link for current page ("Foobar")' ); + assert.equal( hrefC, '/wiki/Foobar', 'Default title; Get link for current page ("Foobar")' ); }); -test( 'wikiScript', function() { - expect(2); - +QUnit.test( 'wikiScript', 4, function ( assert ) { mw.config.set({ - 'wgScript': '/w/index.php', + 'wgScript': '/w/i.php', // customized wgScript for bug 39103 + 'wgLoadScript': '/w/l.php', // customized wgLoadScript for bug 39103 'wgScriptPath': '/w', 'wgScriptExtension': '.php' }); - equal( mw.util.wikiScript(), mw.config.get( 'wgScript' ), 'Defaults to index.php and is equal to wgScript' ); - equal( mw.util.wikiScript( 'api' ), '/w/api.php', 'API path' ); + assert.equal( mw.util.wikiScript(), mw.config.get( 'wgScript' ), 'wikiScript() returns wgScript' ); + assert.equal( mw.util.wikiScript( 'index' ), mw.config.get( 'wgScript' ), "wikiScript( 'index' ) returns wgScript" ); + assert.equal( mw.util.wikiScript( 'load' ), mw.config.get( 'wgLoadScript' ), "wikiScript( 'load' ) returns wgLoadScript" ); + assert.equal( mw.util.wikiScript( 'api' ), '/w/api.php', 'API path' ); }); -test( 'addCSS', function() { - expect(3); - +QUnit.test( 'addCSS', 3, function ( assert ) { var $testEl = $( '<div>' ).attr( 'id', 'mw-addcsstest' ).appendTo( '#qunit-fixture' ); var style = mw.util.addCSS( '#mw-addcsstest { visibility: hidden; }' ); - equal( typeof style, 'object', 'addCSS returned an object' ); - strictEqual( style.disabled, false, 'property "disabled" is available and set to false' ); + assert.equal( typeof style, 'object', 'addCSS returned an object' ); + assert.strictEqual( style.disabled, false, 'property "disabled" is available and set to false' ); - equal( $testEl.css( 'visibility' ), 'hidden', 'Added style properties are in effect' ); + assert.equal( $testEl.css( 'visibility' ), 'hidden', 'Added style properties are in effect' ); // Clean up $( style.ownerNode ).remove(); }); -test( 'toggleToc', function() { - expect(4); - - strictEqual( mw.util.toggleToc(), null, 'Return null if there is no table of contents on the page.' ); +QUnit.asyncTest( 'toggleToc', 4, function ( assert ) { + assert.strictEqual( mw.util.toggleToc(), null, 'Return null if there is no table of contents on the page.' ); var tocHtml = '<table id="toc" class="toc"><tr><td>' + @@ -80,57 +65,46 @@ test( 'toggleToc', function() { $toc = $(tocHtml).appendTo( '#qunit-fixture' ), $toggleLink = $( '#togglelink' ); - strictEqual( $toggleLink.length, 1, 'Toggle link is appended to the page.' ); - - // Toggle animation is asynchronous - // QUnit should not finish this test() untill they are all done - stop(); + assert.strictEqual( $toggleLink.length, 1, 'Toggle link is appended to the page.' ); var actionC = function() { - start(); + QUnit.start(); }; var actionB = function() { - start(); stop(); - strictEqual( mw.util.toggleToc( $toggleLink, actionC ), true, 'Return boolean true if the TOC is now visible.' ); + assert.strictEqual( mw.util.toggleToc( $toggleLink, actionC ), true, 'Return boolean true if the TOC is now visible.' ); }; var actionA = function() { - strictEqual( mw.util.toggleToc( $toggleLink, actionB ), false, 'Return boolean false if the TOC is now hidden.' ); + assert.strictEqual( mw.util.toggleToc( $toggleLink, actionB ), false, 'Return boolean false if the TOC is now hidden.' ); }; actionA(); }); -test( 'getParamValue', function() { - expect(5); - +QUnit.test( 'getParamValue', 5, function ( assert ) { var url1 = 'http://example.org/?foo=wrong&foo=right#&foo=bad'; - equal( mw.util.getParamValue( 'foo', url1 ), 'right', 'Use latest one, ignore hash' ); - strictEqual( mw.util.getParamValue( 'bar', url1 ), null, 'Return null when not found' ); + assert.equal( mw.util.getParamValue( 'foo', url1 ), 'right', 'Use latest one, ignore hash' ); + assert.strictEqual( mw.util.getParamValue( 'bar', url1 ), null, 'Return null when not found' ); var url2 = 'http://example.org/#&foo=bad'; - strictEqual( mw.util.getParamValue( 'foo', url2 ), null, 'Ignore hash if param is not in querystring but in hash (bug 27427)' ); + assert.strictEqual( mw.util.getParamValue( 'foo', url2 ), null, 'Ignore hash if param is not in querystring but in hash (bug 27427)' ); var url3 = 'example.org?' + $.param({ 'TEST': 'a b+c' }); - strictEqual( mw.util.getParamValue( 'TEST', url3 ), 'a b+c', 'Bug 30441: getParamValue must understand "+" encoding of space' ); + assert.strictEqual( mw.util.getParamValue( 'TEST', url3 ), 'a b+c', 'Bug 30441: getParamValue must understand "+" encoding of space' ); var url4 = 'example.org?' + $.param({ 'TEST': 'a b+c d' }); // check for sloppy code from r95332 :) - strictEqual( mw.util.getParamValue( 'TEST', url4 ), 'a b+c d', 'Bug 30441: getParamValue must understand "+" encoding of space (multiple spaces)' ); + assert.strictEqual( mw.util.getParamValue( 'TEST', url4 ), 'a b+c d', 'Bug 30441: getParamValue must understand "+" encoding of space (multiple spaces)' ); }); -test( 'tooltipAccessKey', function() { - expect(3); - - equal( typeof mw.util.tooltipAccessKeyPrefix, 'string', 'mw.util.tooltipAccessKeyPrefix must be a string' ); - ok( mw.util.tooltipAccessKeyRegexp instanceof RegExp, 'mw.util.tooltipAccessKeyRegexp instance of RegExp' ); - ok( mw.util.updateTooltipAccessKeys, 'mw.util.updateTooltipAccessKeys' ); +QUnit.test( 'tooltipAccessKey', 3, function ( assert ) { + assert.equal( typeof mw.util.tooltipAccessKeyPrefix, 'string', 'mw.util.tooltipAccessKeyPrefix must be a string' ); + assert.ok( mw.util.tooltipAccessKeyRegexp instanceof RegExp, 'mw.util.tooltipAccessKeyRegexp instance of RegExp' ); + assert.ok( mw.util.updateTooltipAccessKeys, 'mw.util.updateTooltipAccessKeys' ); }); -test( '$content', function() { - expect(2); - - ok( mw.util.$content instanceof jQuery, 'mw.util.$content instance of jQuery' ); - strictEqual( mw.util.$content.length, 1, 'mw.util.$content must have length of 1' ); +QUnit.test( '$content', 2, function ( assert ) { + assert.ok( mw.util.$content instanceof jQuery, 'mw.util.$content instance of jQuery' ); + assert.strictEqual( mw.util.$content.length, 1, 'mw.util.$content must have length of 1' ); }); @@ -138,87 +112,98 @@ test( '$content', function() { * Portlet names are prefixed with 'p-test' to avoid conflict with core * when running the test suite under a wiki page. * Previously, test elements where invisible to the selector since only - * one element can have a given id. + * one element can have a given id. */ -test( 'addPortletLink', function() { - expect(7); - - var mwPanel = '<div id="mw-panel" class="noprint">\ - <h5>Toolbox</h5>\ +QUnit.test( 'addPortletLink', 8, function ( assert ) { + var pTestTb, pCustom, vectorTabs, tbRL, cuQuux, $cuQuux, tbMW, $tbMW, tbRLDM, caFoo; + pTestTb = '\ <div class="portlet" id="p-test-tb">\ + <h5>Toolbox</h5>\ <ul class="body"></ul>\ - </div>\ -</div>', - vectorTabs = '<div id="p-test-views" class="vectorTabs">\ - <h5>Views</h5>\ - <ul></ul>\ -</div>', - $mwPanel = $(mwPanel).appendTo( '#qunit-fixture' ), - $vectorTabs = $(vectorTabs).appendTo( '#qunit-fixture' ); - - var tbRL = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/ResourceLoader', + </div>'; + pCustom = '\ + <div class="portlet" id="p-test-custom">\ + <h5>Views</h5>\ + <ul class="body">\ + <li id="c-foo"><a href="#">Foo</a></li>\ + <li id="c-barmenu">\ + <ul>\ + <li id="c-bar-baz"><a href="#">Baz</a></a>\ + </ul>\ + </li>\ + </ul>\ + </div>'; + vectorTabs = '\ + <div id="p-test-views" class="vectorTabs">\ + <h5>Views</h5>\ + <ul></ul>\ + </div>'; + + $( '#qunit-fixture' ).append( pTestTb, pCustom, vectorTabs ); + + tbRL = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/ResourceLoader', 'ResourceLoader', 't-rl', 'More info about ResourceLoader on MediaWiki.org ', 'l' ); - ok( $.isDomElement( tbRL ), 'addPortletLink returns a valid DOM Element according to $.isDomElement' ); + assert.ok( $.isDomElement( tbRL ), 'addPortletLink returns a valid DOM Element according to $.isDomElement' ); - var tbMW = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/', - 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', tbRL ), - $tbMW = $( tbMW ); + tbMW = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/', + 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', tbRL ); + $tbMW = $( tbMW ); - equal( $tbMW.attr( 'id' ), 't-mworg', 'Link has correct ID set' ); - equal( $tbMW.closest( '.portlet' ).attr( 'id' ), 'p-test-tb', 'Link was inserted within correct portlet' ); - equal( $tbMW.next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing nextnode)' ); + assert.equal( $tbMW.attr( 'id' ), 't-mworg', 'Link has correct ID set' ); + assert.equal( $tbMW.closest( '.portlet' ).attr( 'id' ), 'p-test-tb', 'Link was inserted within correct portlet' ); + assert.equal( $tbMW.next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing nextnode)' ); - var tbRLDM = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM', - 'Default modules', 't-rldm', 'List of all default modules ', 'd', '#t-rl' ); + cuQuux = mw.util.addPortletLink( 'p-test-custom', '#', 'Quux' ); + $cuQuux = $(cuQuux); - equal( $( tbRLDM ).next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing CSS selector)' ); + assert.equal( + $( '#p-test-custom #c-barmenu ul li' ).length, + 1, + 'addPortletLink did not add the item to all <ul> elements in the portlet (bug 35082)' + ); - var caFoo = mw.util.addPortletLink( 'p-test-views', '#', 'Foo' ); + tbRLDM = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM', + 'Default modules', 't-rldm', 'List of all default modules ', 'd', '#t-rl' ); - strictEqual( $tbMW.find( 'span').length, 0, 'No <span> element should be added for porlets without vectorTabs class.' ); - strictEqual( $( caFoo ).find( 'span').length, 1, 'A <span> element should be added for porlets with vectorTabs class.' ); + assert.equal( $( tbRLDM ).next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing CSS selector)' ); - // Clean up - $( [tbRL, tbMW, tbRLDM, caFoo] ).remove(); -}); + caFoo = mw.util.addPortletLink( 'p-test-views', '#', 'Foo' ); -test( 'jsMessage', function() { - expect(1); + assert.strictEqual( $tbMW.find( 'span').length, 0, 'No <span> element should be added for porlets without vectorTabs class.' ); + assert.strictEqual( $( caFoo ).find( 'span').length, 1, 'A <span> element should be added for porlets with vectorTabs class.' ); +}); +QUnit.test( 'jsMessage', 1, function ( assert ) { var a = mw.util.jsMessage( "MediaWiki is <b>Awesome</b>." ); - ok( a, 'Basic checking of return value' ); + assert.ok( a, 'Basic checking of return value' ); // Clean up $( '#mw-js-message' ).remove(); }); -test( 'validateEmail', function() { - expect(6); - - strictEqual( mw.util.validateEmail( "" ), null, 'Should return null for empty string ' ); - strictEqual( mw.util.validateEmail( "user@localhost" ), true, 'Return true for a valid e-mail address' ); +QUnit.test( 'validateEmail', 6, function ( assert ) { + assert.strictEqual( mw.util.validateEmail( "" ), null, 'Should return null for empty string ' ); + assert.strictEqual( mw.util.validateEmail( "user@localhost" ), true, 'Return true for a valid e-mail address' ); // testEmailWithCommasAreInvalids - strictEqual( mw.util.validateEmail( "user,foo@example.org" ), false, 'Emails with commas are invalid' ); - strictEqual( mw.util.validateEmail( "userfoo@ex,ample.org" ), false, 'Emails with commas are invalid' ); + assert.strictEqual( mw.util.validateEmail( "user,foo@example.org" ), false, 'Emails with commas are invalid' ); + assert.strictEqual( mw.util.validateEmail( "userfoo@ex,ample.org" ), false, 'Emails with commas are invalid' ); // testEmailWithHyphens - strictEqual( mw.util.validateEmail( "user-foo@example.org" ), true, 'Emails may contain a hyphen' ); - strictEqual( mw.util.validateEmail( "userfoo@ex-ample.org" ), true, 'Emails may contain a hyphen' ); + assert.strictEqual( mw.util.validateEmail( "user-foo@example.org" ), true, 'Emails may contain a hyphen' ); + assert.strictEqual( mw.util.validateEmail( "userfoo@ex-ample.org" ), true, 'Emails may contain a hyphen' ); }); -test( 'isIPv6Address', function() { - expect(40); - +QUnit.test( 'isIPv6Address', 40, function ( assert ) { // Shortcuts - var assertFalseIPv6 = function( addy, summary ) { - return strictEqual( mw.util.isIPv6Address( addy ), false, summary ); - }, - assertTrueIPv6 = function( addy, summary ) { - return strictEqual( mw.util.isIPv6Address( addy ), true, summary ); - }; + function assertFalseIPv6( addy, summary ) { + return assert.strictEqual( mw.util.isIPv6Address( addy ), false, summary ); + } + function assertTrueIPv6( addy, summary ) { + return assert.strictEqual( mw.util.isIPv6Address( addy ), true, summary ); + } // Based on IPTest.php > testisIPv6 assertFalseIPv6( ':fc:100::', 'IPv6 starting with lone ":"' ); @@ -232,7 +217,7 @@ test( 'isIPv6Address', function() { 'fc:100:a:d::', 'fc:100:a:d:1::', 'fc:100:a:d:1:e::', - 'fc:100:a:d:1:e:ac::'], function( i, addy ){ + 'fc:100:a:d:1:e:ac::'], function ( i, addy ){ assertTrueIPv6( addy, addy + ' is a valid IP' ); }); @@ -253,7 +238,7 @@ test( 'isIPv6Address', function() { '::fc:100:a:d:1:e', '::fc:100:a:d:1:e:ac', - 'fc:100:a:d:1:e:ac:0'], function( i, addy ){ + 'fc:100:a:d:1:e:ac:0'], function ( i, addy ){ assertTrueIPv6( addy, addy + ' is a valid IP' ); }); @@ -278,16 +263,14 @@ test( 'isIPv6Address', function() { assertFalseIPv6( 'fc::100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' ); }); -test( 'isIPv4Address', function() { - expect(11); - +QUnit.test( 'isIPv4Address', 11, function ( assert ) { // Shortcuts - var assertFalseIPv4 = function( addy, summary ) { - return strictEqual( mw.util.isIPv4Address( addy ), false, summary ); - }, - assertTrueIPv4 = function( addy, summary ) { - return strictEqual( mw.util.isIPv4Address( addy ), true, summary ); - }; + function assertFalseIPv4( addy, summary ) { + assert.strictEqual( mw.util.isIPv4Address( addy ), false, summary ); + } + function assertTrueIPv4( addy, summary ) { + assert.strictEqual( mw.util.isIPv4Address( addy ), true, summary ); + } // Based on IPTest.php > testisIPv4 assertFalseIPv4( false, 'Boolean false is not an IP' ); diff --git a/tests/selenium/SeleniumConfig.php b/tests/selenium/SeleniumConfig.php index b1487154..04cf8d88 100644 --- a/tests/selenium/SeleniumConfig.php +++ b/tests/selenium/SeleniumConfig.php @@ -24,12 +24,7 @@ class SeleniumConfig { throw new MWException( "Unable to read local Selenium Settings from " . $seleniumConfigFile . "\n" ); } - if ( !defined( 'PHP_VERSION_ID' ) || - ( PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION < 3 ) ) { - $configArray = self::parse_5_2_ini_file( $seleniumConfigFile ); - } else { - $configArray = parse_ini_file( $seleniumConfigFile, true ); - } + $configArray = parse_ini_file( $seleniumConfigFile, true ); if ( $configArray === false ) { throw new MWException( "Error parsing " . $seleniumConfigFile . "\n" ); } @@ -61,35 +56,6 @@ class SeleniumConfig { return true; } - /** - * PHP 5.2 parse_ini_file() doesn't have support for array keys. - * This function parses simple ini files with such syntax using just - * 5.2 functions. - */ - private static function parse_5_2_ini_file( $ConfigFile ) { - $file = fopen( $ConfigFile, "rt" ); - if ( !$file ) { - return false; - } - $header = ''; - - $configArray = array(); - - while ( ( $line = fgets( $file ) ) !== false ) { - $line = strtok( $line, "\r\n" ); - - if ( !$line || $line[0] == ';' ) continue; - - if ( $line[0] == '[' && substr( $line, -1 ) == ']' ) { - $header = substr( $line, 1, -1 ); - $configArray[$header] = array(); - } else { - $configArray[$header] = array_merge_recursive( $configArray[$header], self::parse_ini_line( $line ) ); - } - } - return $configArray; - } - private static function parse_ini_line( $iniLine ) { static $specialValues = array( 'false' => false, 'true' => true, 'null' => null ); list( $key, $value ) = explode( '=', $iniLine, 2 ); diff --git a/tests/selenium/data/SimpleSeleniumTestDB.sql b/tests/selenium/data/SimpleSeleniumTestDB.sql index 1a3196c3..d688c3b9 100644 --- a/tests/selenium/data/SimpleSeleniumTestDB.sql +++ b/tests/selenium/data/SimpleSeleniumTestDB.sql @@ -347,7 +347,7 @@ CREATE TABLE `mw_interwiki` ( LOCK TABLES `mw_interwiki` WRITE; /*!40000 ALTER TABLE `mw_interwiki` DISABLE KEYS */; -INSERT INTO `mw_interwiki` VALUES ('acronym','http://www.acronymfinder.com/af-query.asp?String=exact&Acronym=$1','','',0,0),('advogato','http://www.advogato.org/$1','','',0,0),('annotationwiki','http://www.seedwiki.com/page.cfm?wikiid=368&doc=$1','','',0,0),('arxiv','http://www.arxiv.org/abs/$1','','',0,0),('c2find','http://c2.com/cgi/wiki?FindPage&value=$1','','',0,0),('cache','http://www.google.com/search?q=cache:$1','','',0,0),('commons','http://commons.wikimedia.org/wiki/$1','','',0,0),('corpknowpedia','http://corpknowpedia.org/wiki/index.php/$1','','',0,0),('dictionary','http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1','','',0,0),('disinfopedia','http://www.disinfopedia.org/wiki.phtml?title=$1','','',0,0),('docbook','http://wiki.docbook.org/topic/$1','','',0,0),('doi','http://dx.doi.org/$1','','',0,0),('drumcorpswiki','http://www.drumcorpswiki.com/index.php/$1','','',0,0),('dwjwiki','http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1','','',0,0),('elibre','http://enciclopedia.us.es/index.php/$1','','',0,0),('emacswiki','http://www.emacswiki.org/cgi-bin/wiki.pl?$1','','',0,0),('foldoc','http://foldoc.org/?$1','','',0,0),('foxwiki','http://fox.wikis.com/wc.dll?Wiki~$1','','',0,0),('freebsdman','http://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1','','',0,0),('gej','http://www.esperanto.de/cgi-bin/aktivikio/wiki.pl?$1','','',0,0),('gentoo-wiki','http://gentoo-wiki.com/$1','','',0,0),('google','http://www.google.com/search?q=$1','','',0,0),('googlegroups','http://groups.google.com/groups?q=$1','','',0,0),('hammondwiki','http://www.dairiki.org/HammondWiki/$1','','',0,0),('hewikisource','http://he.wikisource.org/wiki/$1','','',1,0),('hrwiki','http://www.hrwiki.org/index.php/$1','','',0,0),('imdb','http://us.imdb.com/Title?$1','','',0,0),('jargonfile','http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1','','',0,0),('jspwiki','http://www.jspwiki.org/wiki/$1','','',0,0),('keiki','http://kei.ki/en/$1','','',0,0),('kmwiki','http://kmwiki.wikispaces.com/$1','','',0,0),('linuxwiki','http://linuxwiki.de/$1','','',0,0),('lojban','http://www.lojban.org/tiki/tiki-index.php?page=$1','','',0,0),('lqwiki','http://wiki.linuxquestions.org/wiki/$1','','',0,0),('lugkr','http://lug-kr.sourceforge.net/cgi-bin/lugwiki.pl?$1','','',0,0),('mathsongswiki','http://SeedWiki.com/page.cfm?wikiid=237&doc=$1','','',0,0),('meatball','http://www.usemod.com/cgi-bin/mb.pl?$1','','',0,0),('mediawikiwiki','http://www.mediawiki.org/wiki/$1','','',0,0),('mediazilla','https://bugzilla.wikimedia.org/$1','','',1,0),('memoryalpha','http://www.memory-alpha.org/en/index.php/$1','','',0,0),('metawiki','http://sunir.org/apps/meta.pl?$1','','',0,0),('metawikipedia','http://meta.wikimedia.org/wiki/$1','','',0,0),('moinmoin','http://purl.net/wiki/moin/$1','','',0,0),('mozillawiki','http://wiki.mozilla.org/index.php/$1','','',0,0),('mw','http://www.mediawiki.org/wiki/$1','','',0,0),('oeis','http://www.research.att.com/cgi-bin/access.cgi/as/njas/sequences/eisA.cgi?Anum=$1','','',0,0),('openfacts','http://openfacts.berlios.de/index.phtml?title=$1','','',0,0),('openwiki','http://openwiki.com/?$1','','',0,0),('pmeg','http://www.bertilow.com/pmeg/$1.php','','',0,0),('ppr','http://c2.com/cgi/wiki?$1','','',0,0),('pythoninfo','http://wiki.python.org/moin/$1','','',0,0),('rfc','http://www.rfc-editor.org/rfc/rfc$1.txt','','',0,0),('s23wiki','http://is-root.de/wiki/index.php/$1','','',0,0),('seattlewiki','http://seattle.wikia.com/wiki/$1','','',0,0),('seattlewireless','http://seattlewireless.net/?$1','','',0,0),('senseislibrary','http://senseis.xmp.net/?$1','','',0,0),('sourceforge','http://sourceforge.net/$1','','',0,0),('squeak','http://wiki.squeak.org/squeak/$1','','',0,0),('susning','http://www.susning.nu/$1','','',0,0),('svgwiki','http://wiki.svg.org/$1','','',0,0),('tavi','http://tavi.sourceforge.net/$1','','',0,0),('tejo','http://www.tejo.org/vikio/$1','','',0,0),('theopedia','http://www.theopedia.com/$1','','',0,0),('tmbw','http://www.tmbw.net/wiki/$1','','',0,0),('tmnet','http://www.technomanifestos.net/?$1','','',0,0),('tmwiki','http://www.EasyTopicMaps.com/?page=$1','','',0,0),('twiki','http://twiki.org/cgi-bin/view/$1','','',0,0),('uea','http://www.tejo.org/uea/$1','','',0,0),('unreal','http://wiki.beyondunreal.com/wiki/$1','','',0,0),('usemod','http://www.usemod.com/cgi-bin/wiki.pl?$1','','',0,0),('vinismo','http://vinismo.com/en/$1','','',0,0),('webseitzwiki','http://webseitz.fluxent.com/wiki/$1','','',0,0),('why','http://clublet.com/c/c/why?$1','','',0,0),('wiki','http://c2.com/cgi/wiki?$1','','',0,0),('wikia','http://www.wikia.com/wiki/$1','','',0,0),('wikibooks','http://en.wikibooks.org/wiki/$1','','',1,0),('wikicities','http://www.wikia.com/wiki/$1','','',0,0),('wikif1','http://www.wikif1.org/$1','','',0,0),('wikihow','http://www.wikihow.com/$1','','',0,0),('wikimedia','http://wikimediafoundation.org/wiki/$1','','',0,0),('wikinews','http://en.wikinews.org/wiki/$1','','',1,0),('wikinfo','http://www.wikinfo.org/index.php/$1','','',0,0),('wikipedia','http://en.wikipedia.org/wiki/$1','','',1,0),('wikiquote','http://en.wikiquote.org/wiki/$1','','',1,0),('wikisource','http://wikisource.org/wiki/$1','','',1,0),('wikispecies','http://species.wikimedia.org/wiki/$1','','',1,0),('wikitravel','http://wikitravel.org/en/$1','','',0,0),('wikiversity','http://en.wikiversity.org/wiki/$1','','',1,0),('wikt','http://en.wiktionary.org/wiki/$1','','',1,0),('wiktionary','http://en.wiktionary.org/wiki/$1','','',1,0),('wlug','http://www.wlug.org.nz/$1','','',0,0),('zwiki','http://zwiki.org/$1','','',0,0),('zzz wiki','http://wiki.zzz.ee/index.php/$1','','',0,0); +INSERT INTO `mw_interwiki` VALUES ('acronym','http://www.acronymfinder.com/af-query.asp?String=exact&Acronym=$1','','',0,0),('advogato','http://www.advogato.org/$1','','',0,0),('annotationwiki','http://www.seedwiki.com/page.cfm?wikiid=368&doc=$1','','',0,0),('arxiv','http://www.arxiv.org/abs/$1','','',0,0),('c2find','http://c2.com/cgi/wiki?FindPage&value=$1','','',0,0),('cache','http://www.google.com/search?q=cache:$1','','',0,0),('commons','http://commons.wikimedia.org/wiki/$1','','',0,0),('corpknowpedia','http://corpknowpedia.org/wiki/index.php/$1','','',0,0),('dictionary','http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1','','',0,0),('disinfopedia','http://www.disinfopedia.org/wiki.phtml?title=$1','','',0,0),('docbook','http://wiki.docbook.org/topic/$1','','',0,0),('doi','http://dx.doi.org/$1','','',0,0),('drumcorpswiki','http://www.drumcorpswiki.com/index.php/$1','','',0,0),('dwjwiki','http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1','','',0,0),('elibre','http://enciclopedia.us.es/index.php/$1','','',0,0),('emacswiki','http://www.emacswiki.org/cgi-bin/wiki.pl?$1','','',0,0),('foldoc','http://foldoc.org/?$1','','',0,0),('foxwiki','http://fox.wikis.com/wc.dll?Wiki~$1','','',0,0),('freebsdman','http://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1','','',0,0),('gej','http://www.esperanto.de/cgi-bin/aktivikio/wiki.pl?$1','','',0,0),('gentoo-wiki','http://gentoo-wiki.com/$1','','',0,0),('google','http://www.google.com/search?q=$1','','',0,0),('googlegroups','http://groups.google.com/groups?q=$1','','',0,0),('hammondwiki','http://www.dairiki.org/HammondWiki/$1','','',0,0),('hewikisource','http://he.wikisource.org/wiki/$1','','',1,0),('hrwiki','http://www.hrwiki.org/index.php/$1','','',0,0),('imdb','http://us.imdb.com/Title?$1','','',0,0),('jargonfile','http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1','','',0,0),('jspwiki','http://www.jspwiki.org/wiki/$1','','',0,0),('keiki','http://kei.ki/en/$1','','',0,0),('kmwiki','http://kmwiki.wikispaces.com/$1','','',0,0),('linuxwiki','http://linuxwiki.de/$1','','',0,0),('lojban','http://www.lojban.org/tiki/tiki-index.php?page=$1','','',0,0),('lqwiki','http://wiki.linuxquestions.org/wiki/$1','','',0,0),('lugkr','http://lug-kr.sourceforge.net/cgi-bin/lugwiki.pl?$1','','',0,0),('mathsongswiki','http://SeedWiki.com/page.cfm?wikiid=237&doc=$1','','',0,0),('meatball','http://www.usemod.com/cgi-bin/mb.pl?$1','','',0,0),('mediawikiwiki','http://www.mediawiki.org/wiki/$1','','',0,0),('mediazilla','https://bugzilla.wikimedia.org/$1','','',1,0),('memoryalpha','http://www.memory-alpha.org/en/index.php/$1','','',0,0),('metawiki','http://sunir.org/apps/meta.pl?$1','','',0,0),('metawikimedia','http://meta.wikimedia.org/wiki/$1','','',0,0),('moinmoin','http://purl.net/wiki/moin/$1','','',0,0),('mozillawiki','http://wiki.mozilla.org/index.php/$1','','',0,0),('mw','http://www.mediawiki.org/wiki/$1','','',0,0),('oeis','http://www.research.att.com/cgi-bin/access.cgi/as/njas/sequences/eisA.cgi?Anum=$1','','',0,0),('openfacts','http://openfacts.berlios.de/index.phtml?title=$1','','',0,0),('openwiki','http://openwiki.com/?$1','','',0,0),('pmeg','http://www.bertilow.com/pmeg/$1.php','','',0,0),('ppr','http://c2.com/cgi/wiki?$1','','',0,0),('pythoninfo','http://wiki.python.org/moin/$1','','',0,0),('rfc','http://www.rfc-editor.org/rfc/rfc$1.txt','','',0,0),('s23wiki','http://is-root.de/wiki/index.php/$1','','',0,0),('seattlewiki','http://seattle.wikia.com/wiki/$1','','',0,0),('seattlewireless','http://seattlewireless.net/?$1','','',0,0),('senseislibrary','http://senseis.xmp.net/?$1','','',0,0),('sourceforge','http://sourceforge.net/$1','','',0,0),('squeak','http://wiki.squeak.org/squeak/$1','','',0,0),('susning','http://www.susning.nu/$1','','',0,0),('svgwiki','http://wiki.svg.org/$1','','',0,0),('tavi','http://tavi.sourceforge.net/$1','','',0,0),('tejo','http://www.tejo.org/vikio/$1','','',0,0),('theopedia','http://www.theopedia.com/$1','','',0,0),('tmbw','http://www.tmbw.net/wiki/$1','','',0,0),('tmnet','http://www.technomanifestos.net/?$1','','',0,0),('tmwiki','http://www.EasyTopicMaps.com/?page=$1','','',0,0),('twiki','http://twiki.org/cgi-bin/view/$1','','',0,0),('uea','http://www.tejo.org/uea/$1','','',0,0),('unreal','http://wiki.beyondunreal.com/wiki/$1','','',0,0),('usemod','http://www.usemod.com/cgi-bin/wiki.pl?$1','','',0,0),('vinismo','http://vinismo.com/en/$1','','',0,0),('webseitzwiki','http://webseitz.fluxent.com/wiki/$1','','',0,0),('why','http://clublet.com/c/c/why?$1','','',0,0),('wiki','http://c2.com/cgi/wiki?$1','','',0,0),('wikia','http://www.wikia.com/wiki/$1','','',0,0),('wikibooks','http://en.wikibooks.org/wiki/$1','','',1,0),('wikicities','http://www.wikia.com/wiki/$1','','',0,0),('wikif1','http://www.wikif1.org/$1','','',0,0),('wikihow','http://www.wikihow.com/$1','','',0,0),('wikimedia','http://wikimediafoundation.org/wiki/$1','','',0,0),('wikinews','http://en.wikinews.org/wiki/$1','','',1,0),('wikinfo','http://www.wikinfo.org/index.php/$1','','',0,0),('wikipedia','http://en.wikipedia.org/wiki/$1','','',1,0),('wikiquote','http://en.wikiquote.org/wiki/$1','','',1,0),('wikisource','http://wikisource.org/wiki/$1','','',1,0),('wikispecies','http://species.wikimedia.org/wiki/$1','','',1,0),('wikitravel','http://wikitravel.org/en/$1','','',0,0),('wikiversity','http://en.wikiversity.org/wiki/$1','','',1,0),('wikt','http://en.wiktionary.org/wiki/$1','','',1,0),('wiktionary','http://en.wiktionary.org/wiki/$1','','',1,0),('wlug','http://www.wlug.org.nz/$1','','',0,0),('zwiki','http://zwiki.org/$1','','',0,0),('zzz wiki','http://wiki.zzz.ee/index.php/$1','','',0,0); /*!40000 ALTER TABLE `mw_interwiki` ENABLE KEYS */; UNLOCK TABLES; diff --git a/tests/selenium/data/mediawiki118_fresh_installation.sql b/tests/selenium/data/mediawiki118_fresh_installation.sql index 2724bad5..7beb9e6a 100644 --- a/tests/selenium/data/mediawiki118_fresh_installation.sql +++ b/tests/selenium/data/mediawiki118_fresh_installation.sql @@ -391,7 +391,7 @@ INSERT INTO `mw_interwiki` VALUES ('mediawikiwiki','http://www.mediawiki.org/wik INSERT INTO `mw_interwiki` VALUES ('mediazilla','https://bugzilla.wikimedia.org/$1','','',1,0); INSERT INTO `mw_interwiki` VALUES ('memoryalpha','http://www.memory-alpha.org/en/index.php/$1','','',0,0); INSERT INTO `mw_interwiki` VALUES ('metawiki','http://sunir.org/apps/meta.pl?$1','','',0,0); -INSERT INTO `mw_interwiki` VALUES ('metawikipedia','http://meta.wikimedia.org/wiki/$1','','',0,0); +INSERT INTO `mw_interwiki` VALUES ('metawikimedia','http://meta.wikimedia.org/wiki/$1','','',0,0); INSERT INTO `mw_interwiki` VALUES ('moinmoin','http://purl.net/wiki/moin/$1','','',0,0); INSERT INTO `mw_interwiki` VALUES ('mozillawiki','http://wiki.mozilla.org/index.php/$1','','',0,0); INSERT INTO `mw_interwiki` VALUES ('mw','http://www.mediawiki.org/wiki/$1','','',0,0); diff --git a/tests/selenium/installer/MediaWikiButtonsAvailabilityTestCase.php b/tests/selenium/installer/MediaWikiButtonsAvailabilityTestCase.php index bf5b379d..8bca4b0d 100644 --- a/tests/selenium/installer/MediaWikiButtonsAvailabilityTestCase.php +++ b/tests/selenium/installer/MediaWikiButtonsAvailabilityTestCase.php @@ -28,7 +28,7 @@ */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 30 (http://www.mediawiki.org/wiki/New_installer/Test_plan) @@ -99,4 +99,4 @@ class MediaWikiButtonsAvailabilityTestCase extends MediaWikiInstallationCommonFu $this->assertTrue( $this->isElementPresent( "submit-back" )); $this->assertTrue( $this->isElementPresent( "submit-continue" )); } -}
\ No newline at end of file +} diff --git a/tests/selenium/installer/MediaWikiDifferentDatabaseAccountTestCase.php b/tests/selenium/installer/MediaWikiDifferentDatabaseAccountTestCase.php index f1b79459..8e2afe73 100644 --- a/tests/selenium/installer/MediaWikiDifferentDatabaseAccountTestCase.php +++ b/tests/selenium/installer/MediaWikiDifferentDatabaseAccountTestCase.php @@ -28,7 +28,7 @@ */ -require_once ( dirname( __FILE__ ) . '/MediaWikiInstallationCommonFunction.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationCommonFunction.php' ); /** * Test Case ID : 04 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiDifferntDatabasePrefixTestCase.php b/tests/selenium/installer/MediaWikiDifferntDatabasePrefixTestCase.php index 2d623afc..55ad4612 100644 --- a/tests/selenium/installer/MediaWikiDifferntDatabasePrefixTestCase.php +++ b/tests/selenium/installer/MediaWikiDifferntDatabasePrefixTestCase.php @@ -27,7 +27,7 @@ * */ -require_once ( dirname( __FILE__ ) . '/MediaWikiInstallationCommonFunction.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationCommonFunction.php' ); /** * Test Case ID : 02 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiErrorsConnectToDatabasePageTestCase.php b/tests/selenium/installer/MediaWikiErrorsConnectToDatabasePageTestCase.php index b112bc0e..825ca424 100644 --- a/tests/selenium/installer/MediaWikiErrorsConnectToDatabasePageTestCase.php +++ b/tests/selenium/installer/MediaWikiErrorsConnectToDatabasePageTestCase.php @@ -28,7 +28,7 @@ */ -require_once ( dirname( __FILE__ ) . '/MediaWikiInstallationCommonFunction.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationCommonFunction.php' ); /** * Test Case ID : 09 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiErrorsNamepageTestCase.php b/tests/selenium/installer/MediaWikiErrorsNamepageTestCase.php index 024fe5d6..c2b35054 100644 --- a/tests/selenium/installer/MediaWikiErrorsNamepageTestCase.php +++ b/tests/selenium/installer/MediaWikiErrorsNamepageTestCase.php @@ -33,7 +33,7 @@ * Version : MediaWiki 1.18alpha */ -require_once ( dirname( __FILE__ ) . '/MediaWikiInstallationCommonFunction.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationCommonFunction.php' ); class MediaWikiErrorsNamepageTestCase extends MediaWikiInstallationCommonFunction { diff --git a/tests/selenium/installer/MediaWikiHelpFieldHintTestCase.php b/tests/selenium/installer/MediaWikiHelpFieldHintTestCase.php index 806fcfde..78205cf8 100644 --- a/tests/selenium/installer/MediaWikiHelpFieldHintTestCase.php +++ b/tests/selenium/installer/MediaWikiHelpFieldHintTestCase.php @@ -33,7 +33,7 @@ * Version : MediaWiki 1.18alpha */ -require_once ( dirname( __FILE__ ) . '/MediaWikiInstallationCommonFunction.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationCommonFunction.php' ); class MediaWikiHelpFieldHintTestCase extends MediaWikiInstallationCommonFunction { diff --git a/tests/selenium/installer/MediaWikiInstallationCommonFunction.php b/tests/selenium/installer/MediaWikiInstallationCommonFunction.php index 99df8a2a..353fa2ee 100644 --- a/tests/selenium/installer/MediaWikiInstallationCommonFunction.php +++ b/tests/selenium/installer/MediaWikiInstallationCommonFunction.php @@ -27,9 +27,9 @@ */ require_once 'PHPUnit/Extensions/SeleniumTestCase.php'; -require_once ( dirname( __FILE__ ) . '/MediaWikiInstallationConfig.php' ); -require_once ( dirname(__FILE__) . '/MediaWikiInstallationMessage.php' ); -require_once ( dirname(__FILE__) . '/MediaWikiInstallationVariables.php'); +require_once ( __DIR__ . '/MediaWikiInstallationConfig.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationMessage.php' ); +require_once ( __DIR__ . '/MediaWikiInstallationVariables.php'); class MediaWikiInstallationCommonFunction extends PHPUnit_Extensions_SeleniumTestCase { diff --git a/tests/selenium/installer/MediaWikiInstallerTestSuite.php b/tests/selenium/installer/MediaWikiInstallerTestSuite.php index 386a50ea..58ccc7cd 100644 --- a/tests/selenium/installer/MediaWikiInstallerTestSuite.php +++ b/tests/selenium/installer/MediaWikiInstallerTestSuite.php @@ -30,19 +30,19 @@ require_once 'PHPUnit/Framework.php'; require_once 'PHPUnit/Framework/TestSuite.php'; -require_once ( dirname( __FILE__ ) . '/MediaWikiUserInterfaceTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiButtonsAvailabilityTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiHelpFieldHintTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiRightFrameworkLinksTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiRestartInstallationTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiErrorsConnectToDatabasePageTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiErrorsNamepageTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiMySQLDataBaseTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiMySQLiteDataBaseTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiUpgradeExistingDatabaseTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiDifferntDatabasePrefixTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiDifferentDatabaseAccountTestCase.php' ); -require_once ( dirname( __FILE__ ) . '/MediaWikiOnAlreadyInstalledTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiUserInterfaceTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiButtonsAvailabilityTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiHelpFieldHintTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiRightFrameworkLinksTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiRestartInstallationTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiErrorsConnectToDatabasePageTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiErrorsNamepageTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiMySQLDataBaseTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiMySQLiteDataBaseTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiUpgradeExistingDatabaseTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiDifferntDatabasePrefixTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiDifferentDatabaseAccountTestCase.php' ); +require_once ( __DIR__ . '/MediaWikiOnAlreadyInstalledTestCase.php' ); diff --git a/tests/selenium/installer/MediaWikiMySQLDataBaseTestCase.php b/tests/selenium/installer/MediaWikiMySQLDataBaseTestCase.php index 399ed4e5..16d065c7 100644 --- a/tests/selenium/installer/MediaWikiMySQLDataBaseTestCase.php +++ b/tests/selenium/installer/MediaWikiMySQLDataBaseTestCase.php @@ -28,7 +28,7 @@ */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 01 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiMySQLiteDataBaseTestCase.php b/tests/selenium/installer/MediaWikiMySQLiteDataBaseTestCase.php index f57c1a55..4ca69162 100644 --- a/tests/selenium/installer/MediaWikiMySQLiteDataBaseTestCase.php +++ b/tests/selenium/installer/MediaWikiMySQLiteDataBaseTestCase.php @@ -28,7 +28,7 @@ */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 06 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiOnAlreadyInstalledTestCase.php b/tests/selenium/installer/MediaWikiOnAlreadyInstalledTestCase.php index 4c052666..7a1b615c 100644 --- a/tests/selenium/installer/MediaWikiOnAlreadyInstalledTestCase.php +++ b/tests/selenium/installer/MediaWikiOnAlreadyInstalledTestCase.php @@ -28,7 +28,7 @@ */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** diff --git a/tests/selenium/installer/MediaWikiRestartInstallationTestCase.php b/tests/selenium/installer/MediaWikiRestartInstallationTestCase.php index b9ca8305..ea87de08 100644 --- a/tests/selenium/installer/MediaWikiRestartInstallationTestCase.php +++ b/tests/selenium/installer/MediaWikiRestartInstallationTestCase.php @@ -29,7 +29,7 @@ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 11, 12 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiRightFrameworkLinksTestCase.php b/tests/selenium/installer/MediaWikiRightFrameworkLinksTestCase.php index 700172c2..7b0fcf36 100644 --- a/tests/selenium/installer/MediaWikiRightFrameworkLinksTestCase.php +++ b/tests/selenium/installer/MediaWikiRightFrameworkLinksTestCase.php @@ -28,7 +28,7 @@ */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 14, 15, 16, 17 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/MediaWikiUpgradeExistingDatabaseTestCase.php b/tests/selenium/installer/MediaWikiUpgradeExistingDatabaseTestCase.php index eb82071e..5cdc8d42 100644 --- a/tests/selenium/installer/MediaWikiUpgradeExistingDatabaseTestCase.php +++ b/tests/selenium/installer/MediaWikiUpgradeExistingDatabaseTestCase.php @@ -28,7 +28,7 @@ */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 05 (http://www.mediawiki.org/wiki/New_installer/Test_plan) @@ -114,4 +114,4 @@ class MediaWikiUpgradeExistingDatabaseTestCase extends MediaWikiInstallationComm $this->chooseCancelOnNextConfirmation(); parent::restartInstallation(); } -}
\ No newline at end of file +} diff --git a/tests/selenium/installer/MediaWikiUserInterfaceTestCase.php b/tests/selenium/installer/MediaWikiUserInterfaceTestCase.php index 0994892f..15fad95f 100644 --- a/tests/selenium/installer/MediaWikiUserInterfaceTestCase.php +++ b/tests/selenium/installer/MediaWikiUserInterfaceTestCase.php @@ -27,7 +27,7 @@ * */ -require_once (dirname(__FILE__).'/'.'MediaWikiInstallationCommonFunction.php'); +require_once (__DIR__.'/'.'MediaWikiInstallationCommonFunction.php'); /** * Test Case ID : 18 - 27 (http://www.mediawiki.org/wiki/New_installer/Test_plan) diff --git a/tests/selenium/installer/README.txt b/tests/selenium/installer/README.txt index 83d1a346..bc880a8b 100644 --- a/tests/selenium/installer/README.txt +++ b/tests/selenium/installer/README.txt @@ -1,6 +1,6 @@ == Details== -Automated Selenium test scripts written for MediaWiki Installer is available at svn.wikimedia.org/svnroot/mediawiki/trunk/phase3/tests/selenium/installer. +Automated Selenium test scripts written for MediaWiki Installer is available at https://gerrit.wikimedia.org/r/gitweb?p=mediawiki/core.git;a=tree;f=tests/selenium/installer;hb=HEAD. Detailed test cases available at http://www.mediawiki.org/wiki/New_installer/Test_plan. Version : MediaWiki 1.18alpha diff --git a/tests/selenium/suites/MediawikiCoreSmokeTestCase.php b/tests/selenium/suites/MediawikiCoreSmokeTestCase.php index 5fc1a5a6..6b8fc974 100644 --- a/tests/selenium/suites/MediawikiCoreSmokeTestCase.php +++ b/tests/selenium/suites/MediawikiCoreSmokeTestCase.php @@ -43,7 +43,7 @@ class MediawikiCoreSmokeTestCase extends SeleniumTestCase { $this->login(); $this->open( $this->getUrl() . '/index.php?title=Special:Upload' ); - $this->type( 'wpUploadFile', dirname( __FILE__ ) . + $this->type( 'wpUploadFile', __DIR__ . "\\..\\data\\Wikipedia-logo-v2-de.png" ); $this->check( 'wpIgnoreWarning' ); $this->click( 'wpUpload' ); diff --git a/tests/selenium/suites/MyContributionsTestCase.php b/tests/selenium/suites/MyContributionsTestCase.php index 01d87e4b..b8d2d48d 100644 --- a/tests/selenium/suites/MyContributionsTestCase.php +++ b/tests/selenium/suites/MyContributionsTestCase.php @@ -27,7 +27,7 @@ * */ -require_once dirname( dirname( __FILE__ ) ) . '/SeleniumTestConstants.php'; +require_once dirname( __DIR__ ) . '/SeleniumTestConstants.php'; class MyContributionsTestCase extends SeleniumTestCase { diff --git a/tests/selenium/suites/MyWatchListTestCase.php b/tests/selenium/suites/MyWatchListTestCase.php index d1ee3e78..998fab9d 100644 --- a/tests/selenium/suites/MyWatchListTestCase.php +++ b/tests/selenium/suites/MyWatchListTestCase.php @@ -27,7 +27,7 @@ * */ -require_once dirname( dirname( __FILE__ ) ) . '/SeleniumTestConstants.php'; +require_once dirname( __DIR__ ) . '/SeleniumTestConstants.php'; class MyWatchListTestCase extends SeleniumTestCase { diff --git a/tests/testHelpers.inc b/tests/testHelpers.inc index 7fc60a5c..39e18c92 100644 --- a/tests/testHelpers.inc +++ b/tests/testHelpers.inc @@ -460,7 +460,7 @@ class TestFileIterator implements Iterator { } if ( isset ( $this->sectionData[$this->section] ) ) { - throw new MWException( "duplicate section '$section' at line {$this->lineNum} of $this->file\n" ); + throw new MWException( "duplicate section '$this->section' at line {$this->lineNum} of $this->file\n" ); } $this->sectionData[$this->section] = ''; @@ -579,4 +579,4 @@ class DelayedParserTest { $this->fnHooks[] = $fnHook; } -}
\ No newline at end of file +} |