File: MemoizedCallableTest.php

package info (click to toggle)
mediawiki 1%3A1.35.13-1%2Bdeb11u2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 274,932 kB
  • sloc: php: 677,563; javascript: 572,709; sql: 11,565; python: 4,447; xml: 3,145; sh: 892; perl: 788; ruby: 496; pascal: 365; makefile: 128
file content (144 lines) | stat: -rw-r--r-- 3,965 bytes parent folder | download
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
<?php

use Wikimedia\TestingAccessWrapper;

/**
 * PHPUnit 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 = TestingAccessWrapper::newFromObject( new MemoizedCallable( 'abs', 100000 ) );
		$this->assertEquals( 86400, $memoized->ttl );

		$memoized = TestingAccessWrapper::newFromObject( new MemoizedCallable( 'abs', -10 ) );
		$this->assertSame( 1, $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', $a->invokeArgs() );
		$this->assertEquals( 'b', $b->invokeArgs() );

		$a = TestingAccessWrapper::newFromObject( $a );
		$b = TestingAccessWrapper::newFromObject( $b );

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

		$c = new ArrayBackedMemoizedCallable( function () {
			return rand();
		} );
		$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 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 {
	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;
	}
}