1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
<?php
namespace Wikimedia\Tests;
use InvalidArgumentException;
use MediaWikiCoversValidator;
use MemoizedCallable;
use PHPUnit\Framework\TestCase;
use stdClass;
use Wikimedia\TestingAccessWrapper;
/**
* @covers \MemoizedCallable
*/
class MemoizedCallableTest extends TestCase {
use MediaWikiCoversValidator;
/**
* The memoized callable should relate inputs to outputs in the same
* way as the original underlying callable.
*/
public function testReturnValuePassedThrough() {
$mock = $this->getMockBuilder( stdClass::class )
->addMethods( [ 'reverse' ] )->getMock();
$mock->method( 'reverse' )
->willReturnCallback( 'strrev' );
$memoized = new MemoizedCallable( [ $mock, 'reverse' ] );
$this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
}
/**
* Consecutive calls to the memoized callable with the same arguments
* should result in just one invocation of the underlying callable.
*
* @requires extension apcu
*/
public function testCallableMemoized() {
$observer = $this->getMockBuilder( stdClass::class )
->addMethods( [ 'computeSomething' ] )->getMock();
$observer->expects( $this->once() )
->method( 'computeSomething' )
->willReturn( 'ok' );
$memoized = new ArrayBackedMemoizedCallable( [ $observer, 'computeSomething' ] );
// First invocation -- delegates to $observer->computeSomething()
$this->assertEquals( 'ok', $memoized->invoke() );
// Second invocation -- returns memoized result
$this->assertEquals( 'ok', $memoized->invoke() );
}
public function testInvokeVariadic() {
$memoized = new MemoizedCallable( 'sprintf' );
$this->assertEquals(
$memoized->invokeArgs( [ 'this is %s', 'correct' ] ),
$memoized->invoke( 'this is %s', 'correct' )
);
}
public function testShortcutMethod() {
$this->assertEquals(
'this is correct',
MemoizedCallable::call( 'sprintf', [ 'this is %s', 'correct' ] )
);
}
/**
* Outlier TTL values should be coerced to range 1 - 86400.
*/
public function testTTLMaxMin() {
$memoized = TestingAccessWrapper::newFromObject( new MemoizedCallable( 'abs', 100000 ) );
$this->assertEquals( 86400, $memoized->ttl );
$memoized = TestingAccessWrapper::newFromObject( new MemoizedCallable( 'abs', -10 ) );
$this->assertSame( 1, $memoized->ttl );
}
public static function makeA() {
return 'a';
}
public static function makeB() {
return 'b';
}
public static function makeRand() {
return rand();
}
/**
* Closure names should be distinct.
*/
public function testMemoizedClosure() {
$a = new MemoizedCallable( [ self::class, 'makeA' ] );
$b = new MemoizedCallable( [ self::class, 'makeB' ] );
$this->assertEquals( 'a', $a->invokeArgs() );
$this->assertEquals( 'b', $b->invokeArgs() );
$a = TestingAccessWrapper::newFromObject( $a );
$b = TestingAccessWrapper::newFromObject( $b );
$this->assertNotEquals(
$a->callableName,
$b->callableName
);
$c = new ArrayBackedMemoizedCallable( [ self::class, 'makeRand' ] );
$this->assertEquals( $c->invokeArgs(), $c->invokeArgs(), 'memoized random' );
}
public function testNonScalarArguments() {
$memoized = new MemoizedCallable( 'gettype' );
$this->expectExceptionMessage( "non-scalar argument" );
$this->expectException( InvalidArgumentException::class );
$memoized->invoke( (object)[] );
}
public function testUnnamedCallable() {
$this->expectExceptionMessage( 'Cannot memoize unnamed closure' );
$this->expectException( InvalidArgumentException::class );
$memoized = new MemoizedCallable( static function () {
return 'a';
} );
}
public function testNotCallable() {
$this->expectExceptionMessage( "must be an instance of callable" );
$this->expectException( InvalidArgumentException::class );
$memoized = new MemoizedCallable( 14 );
}
}
/**
* A MemoizedCallable subclass that stores function return values
* in an instance property rather than APC or APCu.
*/
class ArrayBackedMemoizedCallable extends MemoizedCallable {
/** @var array */
private $cache = [];
protected function fetchResult( $key, &$success ) {
if ( array_key_exists( $key, $this->cache ) ) {
$success = true;
return $this->cache[$key];
}
$success = false;
return false;
}
protected function storeResult( $key, $result ) {
$this->cache[$key] = $result;
}
}
|