summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/libs/MemoizedCallableTest.php
blob: 9127a30f7a1260c96ae1698336e9df2a47d5360f (plain)
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
<?php
/**
 * A MemoizedCallable subclass that stores function return values
 * in an instance property rather than APC or APCu.
 */
class ArrayBackedMemoizedCallable extends MemoizedCallable {
	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;
	}
}

/**
 * PHP Unit tests for MemoizedCallable class.
 * @covers MemoizedCallable
 */
class MemoizedCallableTest extends PHPUnit\Framework\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 )
			->setMethods( [ 'reverse' ] )->getMock();
		$mock->expects( $this->any() )
			->method( 'reverse' )
			->will( $this->returnCallback( '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 )
			->setMethods( [ 'computeSomething' ] )->getMock();
		$observer->expects( $this->once() )
			->method( 'computeSomething' )
			->will( $this->returnValue( '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() );
	}

	/**
	 * @covers MemoizedCallable::invoke
	 */
	public function testInvokeVariadic() {
		$memoized = new MemoizedCallable( 'sprintf' );
		$this->assertEquals(
			$memoized->invokeArgs( [ 'this is %s', 'correct' ] ),
			$memoized->invoke( 'this is %s', 'correct' )
		);
	}

	/**
	 * @covers MemoizedCallable::call
	 */
	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 = new MemoizedCallable( 'abs', 100000 );
		$this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );

		$memoized = new MemoizedCallable( 'abs', -10 );
		$this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
	}

	/**
	 * Closure names should be distinct.
	 */
	public function testMemoizedClosure() {
		$a = new MemoizedCallable( function () {
			return 'a';
		} );

		$b = new MemoizedCallable( function () {
			return 'b';
		} );

		$this->assertEquals( $a->invokeArgs(), 'a' );
		$this->assertEquals( $b->invokeArgs(), 'b' );

		$this->assertNotEquals(
			$this->readAttribute( $a, 'callableName' ),
			$this->readAttribute( $b, 'callableName' )
		);

		$c = new ArrayBackedMemoizedCallable( function () {
			return rand();
		} );
		$this->assertEquals( $c->invokeArgs(), $c->invokeArgs(), 'memoized random' );
	}

	/**
	 * @expectedExceptionMessage non-scalar argument
	 * @expectedException        InvalidArgumentException
	 */
	public function testNonScalarArguments() {
		$memoized = new MemoizedCallable( 'gettype' );
		$memoized->invoke( new stdClass() );
	}

	/**
	 * @expectedExceptionMessage must be an instance of callable
	 * @expectedException        InvalidArgumentException
	 */
	public function testNotCallable() {
		$memoized = new MemoizedCallable( 14 );
	}
}