diff options
Diffstat (limited to 'tests/phpunit/includes/libs')
-rw-r--r-- | tests/phpunit/includes/libs/CSSMinTest.php | 401 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/GenericArrayObjectTest.php | 280 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/HashRingTest.php | 56 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/IEUrlExtensionTest.php | 173 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/IPSetTest.php | 252 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/JavaScriptMinifierTest.php | 204 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/MWMessagePackTest.php | 75 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/ProcessCacheLRUTest.php | 237 | ||||
-rw-r--r-- | tests/phpunit/includes/libs/RunningStatTest.php | 79 |
9 files changed, 1757 insertions, 0 deletions
diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php new file mode 100644 index 00000000..43c50869 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -0,0 +1,401 @@ +<?php +/** + * This file test the CSSMin library shipped with Mediawiki. + * + * @author Timo Tijhof + */ + +class CSSMinTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $server = 'http://doc.example.org'; + + $this->setMwGlobals( array( + 'wgServer' => $server, + 'wgCanonicalServer' => $server, + ) ); + } + + /** + * @dataProvider provideMinifyCases + * @covers CSSMin::minify + */ + public function testMinify( $code, $expectedOutput ) { + $minified = CSSMin::minify( $code ); + + $this->assertEquals( + $expectedOutput, + $minified, + 'Minified output should be in the form expected.' + ); + } + + public static 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:" "}' ), + ); + } + + /** + * This tests funky parameters to CSSMin::remap. testRemapRemapping tests + * the basic functionality. + * + * @dataProvider provideRemapCases + * @covers CSSMin::remap + */ + public 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 + ); + } + + public static 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://doc.example.org/w/skin/images/bar.png); }', + ), + ); + } + + /** + * This tests basic functionality of CSSMin::remap. testRemapRemapping tests funky parameters. + * + * @dataProvider provideRemapRemappingCases + * @covers CSSMin::remap + */ + public function testRemapRemapping( $message, $input, $expectedOutput ) { + $localPath = __DIR__ . '/../../data/cssmin/'; + $remotePath = 'http://localhost/w/'; + + $realOutput = CSSMin::remap( $input, $localPath, $remotePath ); + + $this->assertEquals( + $expectedOutput, + preg_replace( '/\d+-\d+-\d+T\d+:\d+:\d+Z/', 'timestamp', $realOutput ), + "CSSMin::remap: $message" + ); + } + + public static function provideRemapRemappingCases() { + // red.gif and green.gif are one-pixel 35-byte GIFs. + // large.png is a 35K PNG that should be non-embeddable. + // Full paths start with http://localhost/w/. + // Timestamps in output are replaced with 'timestamp'. + + // data: URIs for red.gif and green.gif + $red = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs='; + $green = 'data:image/gif;base64,R0lGODlhAQABAIAAAACAADAAACwAAAAAAQABAAACAkQBADs='; + + return array( + array( + 'Regular file', + 'foo { background: url(red.gif); }', + 'foo { background: url(http://localhost/w/red.gif?timestamp); }', + ), + array( + 'Regular file (missing)', + 'foo { background: url(theColorOfHerHair.gif); }', + 'foo { background: url(http://localhost/w/theColorOfHerHair.gif); }', + ), + array( + 'Remote URL', + 'foo { background: url(http://example.org/w/foo.png); }', + 'foo { background: url(http://example.org/w/foo.png); }', + ), + array( + 'Protocol-relative remote URL', + 'foo { background: url(//example.org/w/foo.png); }', + 'foo { background: url(//example.org/w/foo.png); }', + ), + array( + 'Remote URL with query', + 'foo { background: url(http://example.org/w/foo.png?query=yes); }', + 'foo { background: url(http://example.org/w/foo.png?query=yes); }', + ), + array( + 'Protocol-relative remote URL with query', + 'foo { background: url(//example.org/w/foo.png?query=yes); }', + 'foo { background: url(//example.org/w/foo.png?query=yes); }', + ), + array( + 'Domain-relative URL', + 'foo { background: url(/static/foo.png); }', + 'foo { background: url(http://doc.example.org/static/foo.png); }', + ), + array( + 'Domain-relative URL with query', + 'foo { background: url(/static/foo.png?query=yes); }', + 'foo { background: url(http://doc.example.org/static/foo.png?query=yes); }', + ), + array( + 'Remote URL (unnecessary quotes not preserved)', + 'foo { background: url("http://example.org/w/foo.png"); }', + 'foo { background: url(http://example.org/w/foo.png); }', + ), + array( + 'Embedded file', + 'foo { /* @embed */ background: url(red.gif); }', + "foo { background: url($red); background: url(http://localhost/w/red.gif?timestamp)!ie; }", + ), + array( + 'Embedded file, other comments before the rule', + "foo { /* Bar. */ /* @embed */ background: url(red.gif); }", + "foo { /* Bar. */ background: url($red); /* Bar. */ background: url(http://localhost/w/red.gif?timestamp)!ie; }", + ), + array( + 'Can not re-embed data: URIs', + "foo { /* @embed */ background: url($red); }", + "foo { background: url($red); }", + ), + array( + 'Can not remap data: URIs', + "foo { background: url($red); }", + "foo { background: url($red); }", + ), + array( + 'Can not embed remote URLs', + 'foo { /* @embed */ background: url(http://example.org/w/foo.png); }', + 'foo { background: url(http://example.org/w/foo.png); }', + ), + array( + 'Embedded file (inline @embed)', + 'foo { background: /* @embed */ url(red.gif); }', + "foo { background: url($red); " + . "background: url(http://localhost/w/red.gif?timestamp)!ie; }", + ), + array( + 'Can not embed large files', + 'foo { /* @embed */ background: url(large.png); }', + "foo { background: url(http://localhost/w/large.png?timestamp); }", + ), + array( + 'Two regular files in one rule', + 'foo { background: url(red.gif), url(green.gif); }', + 'foo { background: url(http://localhost/w/red.gif?timestamp), ' + . 'url(http://localhost/w/green.gif?timestamp); }', + ), + array( + 'Two embedded files in one rule', + 'foo { /* @embed */ background: url(red.gif), url(green.gif); }', + "foo { background: url($red), url($green); " + . "background: url(http://localhost/w/red.gif?timestamp), " + . "url(http://localhost/w/green.gif?timestamp)!ie; }", + ), + array( + 'Two embedded files in one rule (inline @embed)', + 'foo { background: /* @embed */ url(red.gif), /* @embed */ url(green.gif); }', + "foo { background: url($red), url($green); " + . "background: url(http://localhost/w/red.gif?timestamp), " + . "url(http://localhost/w/green.gif?timestamp)!ie; }", + ), + array( + 'Two embedded files in one rule (inline @embed), one too large', + 'foo { background: /* @embed */ url(red.gif), /* @embed */ url(large.png); }', + "foo { background: url($red), url(http://localhost/w/large.png?timestamp); " + . "background: url(http://localhost/w/red.gif?timestamp), " + . "url(http://localhost/w/large.png?timestamp)!ie; }", + ), + array( + 'Practical example with some noise', + 'foo { /* @embed */ background: #f9f9f9 url(red.gif) 0 0 no-repeat; }', + "foo { background: #f9f9f9 url($red) 0 0 no-repeat; " + . "background: #f9f9f9 url(http://localhost/w/red.gif?timestamp) 0 0 no-repeat!ie; }", + ), + array( + 'Does not mess with other properties', + 'foo { color: red; background: url(red.gif); font-size: small; }', + 'foo { color: red; background: url(http://localhost/w/red.gif?timestamp); font-size: small; }', + ), + array( + 'Spacing and miscellanea not changed (1)', + 'foo { background: url(red.gif); }', + 'foo { background: url(http://localhost/w/red.gif?timestamp); }', + ), + array( + 'Spacing and miscellanea not changed (2)', + 'foo {background:url(red.gif)}', + 'foo {background:url(http://localhost/w/red.gif?timestamp)}', + ), + array( + 'Spaces within url() parentheses are ignored', + 'foo { background: url( red.gif ); }', + 'foo { background: url(http://localhost/w/red.gif?timestamp); }', + ), + array( + '@import rule to local file (should we remap this?)', + '@import url(/styles.css)', + '@import url(http://doc.example.org/styles.css)', + ), + array( + '@import rule to URL (should we remap this?)', + '@import url(//localhost/styles.css?query=yes)', + '@import url(//localhost/styles.css?query=yes)', + ), + array( + 'Simple case with comments before url', + 'foo { prop: /* some {funny;} comment */ url(bar.png); }', + 'foo { prop: /* some {funny;} comment */ url(http://localhost/w/bar.png); }', + ), + array( + 'Simple case with comments after url', + 'foo { prop: url(red.gif)/* some {funny;} comment */ ; }', + 'foo { prop: url(http://localhost/w/red.gif?timestamp)/* some {funny;} comment */ ; }', + ), + array( + 'Embedded file with comment before url', + 'foo { /* @embed */ background: /* some {funny;} comment */ url(red.gif); }', + "foo { background: /* some {funny;} comment */ url($red); background: /* some {funny;} comment */ url(http://localhost/w/red.gif?timestamp)!ie; }", + ), + array( + 'Embedded file with comments inside and outside the rule', + 'foo { /* @embed */ background: url(red.gif) /* some {foo;} comment */; /* some {bar;} comment */ }', + "foo { background: url($red) /* some {foo;} comment */; background: url(http://localhost/w/red.gif?timestamp) /* some {foo;} comment */!ie; /* some {bar;} comment */ }", + ), + array( + 'Embedded file with comment outside the rule', + 'foo { /* @embed */ background: url(red.gif); /* some {funny;} comment */ }', + "foo { background: url($red); background: url(http://localhost/w/red.gif?timestamp)!ie; /* some {funny;} comment */ }", + ), + array( + 'Rule with two urls, each with comments', + '{ background: /*asd*/ url(something.png); background: /*jkl*/ url(something.png); }', + '{ background: /*asd*/ url(http://localhost/w/something.png); background: /*jkl*/ url(http://localhost/w/something.png); }', + ), + array( + 'Sanity check for offending line from jquery.ui.theme.css (bug 60077)', + '.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }', + '.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(http://localhost/w/images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }', + ), + ); + } + + /** + * This tests basic functionality of CSSMin::buildUrlValue. + * + * @dataProvider provideBuildUrlValueCases + * @covers CSSMin::buildUrlValue + */ + public function testBuildUrlValue( $message, $input, $expectedOutput ) { + $this->assertEquals( + $expectedOutput, + CSSMin::buildUrlValue( $input ), + "CSSMin::buildUrlValue: $message" + ); + } + + public static function provideBuildUrlValueCases() { + return array( + array( + 'Full URL', + 'scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s', + 'url(scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s)', + ), + array( + 'data: URI', + 'data:image/png;base64,R0lGODlh/+==', + 'url(data:image/png;base64,R0lGODlh/+==)', + ), + array( + 'URL with quotes', + "https://en.wikipedia.org/wiki/Wendy's", + "url(\"https://en.wikipedia.org/wiki/Wendy's\")", + ), + array( + 'URL with parentheses', + 'https://en.wikipedia.org/wiki/Boston_(band)', + 'url("https://en.wikipedia.org/wiki/Boston_(band)")', + ), + ); + } + + /** + * Seperated because they are currently broken (bug 35492) + * + * @group Broken + * @dataProvider provideStringCases + * @covers CSSMin::remap + */ + public function testMinifyWithCSSStringValues( $code, $expectedOutput ) { + $this->testMinifyOutput( $code, $expectedOutput ); + } + + public static 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..4911f73a --- /dev/null +++ b/tests/phpunit/includes/libs/GenericArrayObjectTest.php @@ -0,0 +1,280 @@ +<?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 + */ + abstract public function elementInstancesProvider(); + + /** + * Returns the name of the concrete class being tested. + * + * @since 1.20 + * + * @return string + */ + abstract public 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[0] ); + } + + 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 + * + * @covers GenericArrayObject::__construct + */ + public function testConstructor( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( count( $elements ), $arrayObject->count() ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + * + * @covers GenericArrayObject::isEmpty + */ + public function testIsEmpty( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( $elements === array(), $arrayObject->isEmpty() ); + } + + /** + * @dataProvider instanceProvider + * + * @since 1.20 + * + * @param GenericArrayObject $list + * + * @covers GenericArrayObject::offsetUnset + */ + public function testUnset( GenericArrayObject $list ) { + if ( $list->isEmpty() ) { + $this->assertTrue( true ); // We cannot test unset if there are no elements + } else { + $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() ); + } + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + * + * @covers GenericArrayObject::append + */ + 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 callable $function + * + * @covers GenericArrayObject::getObjectType + */ + 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 + * + * @covers GenericArrayObject::offsetSet + */ + 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 ); + } ); + } + + /** + * @dataProvider instanceProvider + * + * @since 1.21 + * + * @param GenericArrayObject $list + * + * @covers GenericArrayObject::getSerializationData + * @covers GenericArrayObject::serialize + * @covers GenericArrayObject::unserialize + */ + public function testSerialization( GenericArrayObject $list ) { + $serialization = serialize( $list ); + $copy = unserialize( $serialization ); + + $this->assertEquals( $serialization, serialize( $copy ) ); + $this->assertEquals( count( $list ), count( $copy ) ); + + $list = $list->getArrayCopy(); + $copy = $copy->getArrayCopy(); + + $this->assertArrayEquals( $list, $copy, true, true ); + } +} diff --git a/tests/phpunit/includes/libs/HashRingTest.php b/tests/phpunit/includes/libs/HashRingTest.php new file mode 100644 index 00000000..68dfea1f --- /dev/null +++ b/tests/phpunit/includes/libs/HashRingTest.php @@ -0,0 +1,56 @@ +<?php + +/** + * @group HashRing + */ +class HashRingTest extends MediaWikiTestCase { + /** + * @covers HashRing + */ + public function testHashRing() { + $ring = new HashRing( array( 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3 ) ); + + $locations = array(); + for ( $i = 0; $i < 20; $i++ ) { + $locations[ "hello$i"] = $ring->getLocation( "hello$i" ); + } + $expectedLocations = array( + "hello0" => "s5", + "hello1" => "s6", + "hello2" => "s2", + "hello3" => "s5", + "hello4" => "s6", + "hello5" => "s4", + "hello6" => "s5", + "hello7" => "s4", + "hello8" => "s5", + "hello9" => "s5", + "hello10" => "s3", + "hello11" => "s6", + "hello12" => "s1", + "hello13" => "s3", + "hello14" => "s3", + "hello15" => "s5", + "hello16" => "s4", + "hello17" => "s6", + "hello18" => "s6", + "hello19" => "s3" + ); + + $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' ); + + $locations = array(); + for ( $i = 0; $i < 5; $i++ ) { + $locations[ "hello$i"] = $ring->getLocations( "hello$i", 2 ); + } + + $expectedLocations = array( + "hello0" => array( "s5", "s6" ), + "hello1" => array( "s6", "s4" ), + "hello2" => array( "s2", "s1" ), + "hello3" => array( "s5", "s6" ), + "hello4" => array( "s6", "s4" ), + ); + $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' ); + } +} diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php new file mode 100644 index 00000000..b7071230 --- /dev/null +++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php @@ -0,0 +1,173 @@ +<?php + +/** + * Tests for IEUrlExtension::findIE6Extension + * @todo tests below for findIE6Extension should be split into... + * ...a dataprovider and test method. + */ +class IEUrlExtensionTest extends MediaWikiTestCase { + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testSimple() { + $this->assertEquals( + 'y', + IEUrlExtension::findIE6Extension( 'x.y' ), + 'Simple extension' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testSimpleNoExt() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'x' ), + 'No extension' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testEmpty() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '' ), + 'Empty string' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testQuestionMark() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '?' ), + 'Question mark only' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testExtQuestionMark() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '.x?' ), + 'Extension then question mark' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testQuestionMarkExt() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '?.x' ), + 'Question mark then extension' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testInvalidChar() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '.x*' ), + 'Extension with invalid character' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testInvalidCharThenExtension() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '*.x' ), + 'Invalid character followed by an extension' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testMultipleQuestionMarks() { + $this->assertEquals( + 'c', + IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ), + 'Multiple question marks' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testExeException() { + $this->assertEquals( + 'd', + IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ), + '.exe exception' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testExeException2() { + $this->assertEquals( + 'exe', + IEUrlExtension::findIE6Extension( 'a?b?.exe' ), + '.exe exception 2' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testHash() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'a#b.c' ), + 'Hash character preceding extension' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testHash2() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'a?#b.c' ), + 'Hash character preceding extension 2' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testDotAtEnd() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '.' ), + 'Dot at end of string' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testTwoDots() { + $this->assertEquals( + 'z', + IEUrlExtension::findIE6Extension( 'x.y.z' ), + 'Two dots' + ); + } +} diff --git a/tests/phpunit/includes/libs/IPSetTest.php b/tests/phpunit/includes/libs/IPSetTest.php new file mode 100644 index 00000000..d4e5214a --- /dev/null +++ b/tests/phpunit/includes/libs/IPSetTest.php @@ -0,0 +1,252 @@ +<?php + +/** + * @group IPSet + */ +class IPSetTest extends MediaWikiTestCase { + /** + * Provides test cases for IPSetTest::testIPSet + * + * Returns an array of test cases. Each case is an array of (description, + * config, tests). Description is just text output for failure messages, + * config is an array constructor argument for IPSet, and the tests are + * an array of IP => expected (boolean) result against the config dataset. + */ + public static function provideIPSets() { + return array( + array( + 'old_list_subset', + array( + '208.80.152.162', + '10.64.0.123', + '10.64.0.124', + '10.64.0.125', + '10.64.0.126', + '10.64.0.127', + '10.64.0.128', + '10.64.0.129', + '10.64.32.104', + '10.64.32.105', + '10.64.32.106', + '10.64.32.107', + '91.198.174.45', + '91.198.174.46', + '91.198.174.47', + '91.198.174.57', + '2620:0:862:1:A6BA:DBFF:FE30:CFB3', + '91.198.174.58', + '2620:0:862:1:A6BA:DBFF:FE38:FFDA', + '208.80.152.16', + '208.80.152.17', + '208.80.152.18', + '208.80.152.19', + '91.198.174.102', + '91.198.174.103', + '91.198.174.104', + '91.198.174.105', + '91.198.174.106', + '91.198.174.107', + '91.198.174.81', + '2620:0:862:1:26B6:FDFF:FEF5:B2D4', + '91.198.174.82', + '2620:0:862:1:26B6:FDFF:FEF5:ABB4', + '10.20.0.113', + '2620:0:862:102:26B6:FDFF:FEF5:AD9C', + '10.20.0.114', + '2620:0:862:102:26B6:FDFF:FEF5:7C38', + ), + array( + '0.0.0.0' => false, + '255.255.255.255' => false, + '10.64.0.122' => false, + '10.64.0.123' => true, + '10.64.0.124' => true, + '10.64.0.129' => true, + '10.64.0.130' => false, + '91.198.174.81' => true, + '91.198.174.80' => false, + '0::0' => false, + 'ffff:ffff:ffff:ffff:FFFF:FFFF:FFFF:FFFF' => false, + '2001:db8::1234' => false, + '2620:0:862:1:26b6:fdff:fef5:abb3' => false, + '2620:0:862:1:26b6:fdff:fef5:abb4' => true, + '2620:0:862:1:26b6:fdff:fef5:abb5' => false, + ), + ), + array( + 'new_cidr_set', + array( + '208.80.154.0/26', + '2620:0:861:1::/64', + '208.80.154.128/26', + '2620:0:861:2::/64', + '208.80.154.64/26', + '2620:0:861:3::/64', + '208.80.155.96/27', + '2620:0:861:4::/64', + '10.64.0.0/22', + '2620:0:861:101::/64', + '10.64.16.0/22', + '2620:0:861:102::/64', + '10.64.32.0/22', + '2620:0:861:103::/64', + '10.64.48.0/22', + '2620:0:861:107::/64', + '91.198.174.0/25', + '2620:0:862:1::/64', + '10.20.0.0/24', + '2620:0:862:102::/64', + '10.128.0.0/24', + '2620:0:863:101::/64', + '10.2.4.26', + ), + array( + '0.0.0.0' => false, + '255.255.255.255' => false, + '10.2.4.25' => false, + '10.2.4.26' => true, + '10.2.4.27' => false, + '10.20.0.255' => true, + '10.128.0.0' => true, + '10.64.17.55' => true, + '10.64.20.0' => false, + '10.64.27.207' => false, + '10.64.31.255' => false, + '0::0' => false, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => false, + '2001:DB8::1' => false, + '2620:0:861:106::45' => false, + '2620:0:862:103::' => false, + '2620:0:862:102:10:20:0:113' => true, + ), + ), + array( + 'empty_set', + array(), + array( + '0.0.0.0' => false, + '255.255.255.255' => false, + '10.2.4.25' => false, + '10.2.4.26' => false, + '10.2.4.27' => false, + '10.20.0.255' => false, + '10.128.0.0' => false, + '10.64.17.55' => false, + '10.64.20.0' => false, + '10.64.27.207' => false, + '10.64.31.255' => false, + '0::0' => false, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => false, + '2001:DB8::1' => false, + '2620:0:861:106::45' => false, + '2620:0:862:103::' => false, + '2620:0:862:102:10:20:0:113' => false, + ), + ), + array( + 'edge_cases', + array( + '0.0.0.0', + '255.255.255.255', + '::', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + '10.10.10.10/25', // host bits intentional + ), + array( + '0.0.0.0' => true, + '255.255.255.255' => true, + '10.2.4.25' => false, + '10.2.4.26' => false, + '10.2.4.27' => false, + '10.20.0.255' => false, + '10.128.0.0' => false, + '10.64.17.55' => false, + '10.64.20.0' => false, + '10.64.27.207' => false, + '10.64.31.255' => false, + '0::0' => true, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => true, + '2001:DB8::1' => false, + '2620:0:861:106::45' => false, + '2620:0:862:103::' => false, + '2620:0:862:102:10:20:0:113' => false, + '10.10.9.255' => false, + '10.10.10.0' => true, + '10.10.10.1' => true, + '10.10.10.10' => true, + '10.10.10.126' => true, + '10.10.10.127' => true, + '10.10.10.128' => false, + '10.10.10.177' => false, + '10.10.10.255' => false, + '10.10.11.0' => false, + ), + ), + array( + 'exercise_optimizer', + array( + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fffd:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fffb:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fffa:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:8000/113', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:0/113', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff7:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff6:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff5:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff4:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff3:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff2:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff1:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffef:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffee:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffec:0/111', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffeb:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffea:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe9:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe8:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe7:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe6:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe5:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe4:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe3:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe2:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe1:0/112', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/110', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107', + 'ffff:ffff:ffff:ffff:ffff:ffff:ffa0:0/107', + ), + array( + '0.0.0.0' => false, + '255.255.255.255' => false, + '::' => false, + 'ffff:ffff:ffff:ffff:ffff:ffff:ff9f:ffff' => false, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffa0:0' => true, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffc0:1234' => true, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffed:ffff' => true, + 'ffff:ffff:ffff:ffff:ffff:ffff:fff4:4444' => true, + 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:8080' => true, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => true, + ), + ), + ); + } + + /** + * Validates IPSet loading and matching code + * + * @covers IPSet + * @dataProvider provideIPSets + */ + public function testIPSet( $desc, array $cfg, array $tests ) { + $ipset = new IPSet( $cfg ); + foreach ( $tests as $ip => $expected ) { + $result = $ipset->match( $ip ); + $this->assertEquals( $expected, $result, "Incorrect match() result for $ip in dataset $desc" ); + } + } +} diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php new file mode 100644 index 00000000..c8795b2e --- /dev/null +++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php @@ -0,0 +1,204 @@ +<?php + +class JavaScriptMinifierTest extends MediaWikiTestCase { + + public static function provideCases() { + return array( + + // 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 ' . + */ + array( + "' Foo \\' bar \\\n baz \\' quox ' .length", + "' Foo \\' bar \\\n baz \\' quox '.length" + ), + 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;" ), + array( "while(p){continue\nx;}", "while(p){continue\nx;}" ), + array( "while(p){break\nx;}", "while(p){break\nx;}" ), + array( "var\nx;", "var x;" ), + array( "x\ny;", "x\ny;" ), + array( "x\n++y;", "x\n++y;" ), + array( "x\n!y;", "x\n!y;" ), + array( "x\n{y}", "x\n{y}" ), + array( "x\n+y;", "x+y;" ), + array( "x\n(y);", "x(y);" ), + 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})" ), + array( "+/ x/g", "+/ x/g" ), + array( "++/ x/g", "++/ x/g" ), + array( "x/ x/g", "x/x/g" ), + array( "(/ x/g)", "(/ x/g)" ), + array( "if(/ x/g);", "if(/ x/g);" ), + array( "(x/ x/g)", "(x/x/g)" ), + array( "([/ x/g])", "([/ x/g])" ), + array( "+x/ x/g", "+x/x/g" ), + array( "{}/ x/g", "{}/ x/g" ), + array( "+{}/ x/g", "+{}/x/g" ), + array( "(x)/ x/g", "(x)/x/g" ), + array( "if(x)/ x/g", "if(x)/ x/g" ), + array( "for(x;x;{}/ x/g);", "for(x;x;{}/x/g);" ), + array( "x;x;{}/ x/g", "x;x;{}/ x/g" ), + array( "x:{}/ x/g", "x:{}/ x/g" ), + array( "switch(x){case y?z:{}/ x/g:{}/ x/g;}", "switch(x){case y?z:{}/x/g:{}/ x/g;}" ), + array( "function x(){}/ x/g", "function x(){}/ x/g" ), + array( "+function x(){}/ x/g", "+function x(){}/x/g" ), + + // 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');" ), + + // newline insertion after 1000 chars: break after the "++", not before + array( str_repeat( ';', 996 ) . "if(x++);", str_repeat( ';', 996 ) . "if(x++\n);" ), + + // Unicode letter characters should pass through ok in identifiers (bug 31187) + array( "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}' ), + + // 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 + array( "var a = 5.;", "var a=5.;" ), + array( "5.0.toString();", "5.0.toString();" ), + array( "5..toString();", "5..toString();" ), + array( "5...toString();", false ), + array( "5.\n.toString();", '5..toString();' ), + ); + } + + /** + * @dataProvider provideCases + * @covers JavaScriptMinifier::minify + */ + public function testJavaScriptMinifierOutput( $code, $expectedOutput ) { + $minified = JavaScriptMinifier::minify( $code ); + + // JSMin+'s parser will throw an exception if output is not valid JS. + // suppression of warnings needed for stupid crap + wfSuppressWarnings(); + $parser = new JSParser(); + wfRestoreWarnings(); + $parser->parse( $minified, 'minify-test.js', 1 ); + + $this->assertEquals( + $expectedOutput, + $minified, + "Minified output should be in the form expected." + ); + } + + public static function provideBug32548() { + return array( + array( + // This one gets interpreted all together by the prior code; + // no break at the 'E' happens. + '1.23456789E55', + ), + array( + // This one breaks under the bad code; splits between 'E' and '+' + '1.23456789E+5', + ), + array( + // This one breaks under the bad code; splits between 'E' and '-' + '1.23456789E-5', + ), + ); + } + + /** + * @dataProvider provideBug32548 + * @covers JavaScriptMinifier::minify + * @todo give this test a real name explaining what is being tested here + */ + public 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/libs/MWMessagePackTest.php b/tests/phpunit/includes/libs/MWMessagePackTest.php new file mode 100644 index 00000000..f80f78df --- /dev/null +++ b/tests/phpunit/includes/libs/MWMessagePackTest.php @@ -0,0 +1,75 @@ +<?php +/** + * PHP Unit tests for MWMessagePack + * @covers MWMessagePack + */ +class MWMessagePackTest extends MediaWikiTestCase { + + /** + * Provides test cases for MWMessagePackTest::testMessagePack + * + * Returns an array of test cases. Each case is an array of (type, value, + * expected encoding as hex string). The expected values were generated + * using <https://github.com/msgpack/msgpack-php>, which includes a + * serialization function. + */ + public static function providePacks() { + $tests = array( + array( 'nil', null, 'c0' ), + array( 'bool', true, 'c3' ), + array( 'bool', false, 'c2' ), + array( 'positive fixnum', 0, '00' ), + array( 'positive fixnum', 1, '01' ), + array( 'positive fixnum', 5, '05' ), + array( 'positive fixnum', 35, '23' ), + array( 'uint 8', 128, 'cc80' ), + array( 'uint 16', 1000, 'cd03e8' ), + array( 'uint 32', 100000, 'ce000186a0' ), + array( 'negative fixnum', -1, 'ff' ), + array( 'negative fixnum', -2, 'fe' ), + array( 'int 8', -128, 'd080' ), + array( 'int 8', -35, 'd0dd' ), + array( 'int 16', -1000, 'd1fc18' ), + array( 'int 32', -100000, 'd2fffe7960' ), + array( 'double', 0.1, 'cb3fb999999999999a' ), + array( 'double', 1.1, 'cb3ff199999999999a' ), + array( 'double', 123.456, 'cb405edd2f1a9fbe77' ), + array( 'fix raw', '', 'a0' ), + array( 'fix raw', 'foobar', 'a6666f6f626172' ), + array( + 'raw 16', + 'Lorem ipsum dolor sit amet amet.', + 'da00204c6f72656d20697073756d20646f6c6f722073697420616d657420616d65742e' + ), + array( + 'fix array', + array( 'abc', 'def', 'ghi' ), + '93a3616263a3646566a3676869' + ), + array( + 'fix map', + array( 'one' => 1, 'two' => 2 ), + '82a36f6e6501a374776f02' + ), + ); + + if ( PHP_INT_SIZE > 4 ) { + $tests[] = array( 'uint 64', 10000000000, 'cf00000002540be400' ); + $tests[] = array( 'int 64', -10000000000, 'd3fffffffdabf41c00' ); + $tests[] = array( 'int 64', -223372036854775807, 'd3fce66c50e2840001' ); + $tests[] = array( 'int 64', -9223372036854775807, 'd38000000000000001' ); + } + + return $tests; + } + + /** + * Verify that values are serialized correctly. + * @covers MWMessagePack::pack + * @dataProvider providePacks + */ + public function testPack( $type, $value, $expected ) { + $actual = bin2hex( MWMessagePack::pack( $value ) ); + $this->assertEquals( $expected, $actual, $type ); + } +} diff --git a/tests/phpunit/includes/libs/ProcessCacheLRUTest.php b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php new file mode 100644 index 00000000..1a8a1e56 --- /dev/null +++ b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php @@ -0,0 +1,237 @@ +<?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 + */ + public 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 UnexpectedValueException + */ + public function testConstructorGivenInvalidValue( $maxSize ) { + new ProcessCacheLRUTestable( $maxSize ); + } + + /** + * Value which are forbidden by the constructor + */ + public static function provideInvalidConstructorArg() { + return array( + array( null ), + array( array() ), + array( new stdClass() ), + array( 0 ), + array( '5' ), + array( -1 ), + ); + } + + public 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' ) ); + } + + public 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 int $cacheMaxEntries Maximum entry the created cache will hold + * @param int $entryToFill Number of entries to insert in the created cache. + */ + public 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 + */ + public static 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. + */ + public 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() + ); + } + + public 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). + */ + public 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' ) + ); + } + + public 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/libs/RunningStatTest.php b/tests/phpunit/includes/libs/RunningStatTest.php new file mode 100644 index 00000000..dc5db82c --- /dev/null +++ b/tests/phpunit/includes/libs/RunningStatTest.php @@ -0,0 +1,79 @@ +<?php +/** + * PHP Unit tests for RunningStat class. + * @covers RunningStat + */ +class RunningStatTest extends MediaWikiTestCase { + + public $points = array( + 49.7168, 74.3804, 7.0115, 96.5769, 34.9458, + 36.9947, 33.8926, 89.0774, 23.7745, 73.5154, + 86.1322, 53.2124, 16.2046, 73.5130, 10.4209, + 42.7299, 49.3330, 47.0215, 34.9950, 18.2914, + ); + + /** + * Verify that the statistical moments and extrema computed by RunningStat + * match expected values. + * @covers RunningStat::push + * @covers RunningStat::count + * @covers RunningStat::getMean + * @covers RunningStat::getVariance + * @covers RunningStat::getStdDev + */ + public function testRunningStatAccuracy() { + $rstat = new RunningStat(); + foreach( $this->points as $point ) { + $rstat->push( $point ); + } + + $mean = array_sum( $this->points ) / count( $this->points ); + $variance = array_sum( array_map( function ( $x ) use ( $mean ) { + return pow( $mean - $x, 2 ); + }, $this->points ) ) / ( count( $rstat ) - 1 ); + $stddev = sqrt( $variance ); + + $this->assertEquals( count( $rstat ), count( $this->points ) ); + $this->assertEquals( $rstat->min, min( $this->points ) ); + $this->assertEquals( $rstat->max, max( $this->points ) ); + $this->assertEquals( $rstat->getMean(), $mean ); + $this->assertEquals( $rstat->getVariance(), $variance ); + $this->assertEquals( $rstat->getStdDev(), $stddev ); + } + + /** + * When one RunningStat instance is merged into another, the state of the + * target RunningInstance should have the state that it would have had if + * all the data had been accumulated by it alone. + * @covers RunningStat::merge + * @covers RunningStat::count + */ + public function testRunningStatMerge() { + $expected = new RunningStat(); + + foreach( $this->points as $point ) { + $expected->push( $point ); + } + + // Split the data into two sets + $sets = array_chunk( $this->points, floor( count( $this->points ) / 2 ) ); + + // Accumulate the first half into one RunningStat object + $first = new RunningStat(); + foreach( $sets[0] as $point ) { + $first->push( $point ); + } + + // Accumulate the second half into another RunningStat object + $second = new RunningStat(); + foreach( $sets[1] as $point ) { + $second->push( $point ); + } + + // Merge the second RunningStat object into the first + $first->merge( $second ); + + $this->assertEquals( count( $first ), count( $this->points ) ); + $this->assertEquals( $first, $expected ); + } +} |