1<?php
2
3use Wikimedia\TestingAccessWrapper;
4
5/**
6 * @group BagOStuff
7 */
8class HashBagOStuffTest extends PHPUnit\Framework\TestCase {
9	use MediaWikiCoversValidator;
10
11	/**
12	 * @covers HashBagOStuff::__construct
13	 */
14	public function testConstruct() {
15		$this->assertInstanceOf(
16			HashBagOStuff::class,
17			new HashBagOStuff()
18		);
19	}
20
21	/**
22	 * @covers HashBagOStuff::__construct
23	 */
24	public function testQoS() {
25		$bag = new HashBagOStuff();
26
27		$this->assertSame(
28			BagOStuff::QOS_DURABILITY_SCRIPT,
29			$bag->getQoS( BagOStuff::ATTR_DURABILITY )
30		);
31
32		$this->assertSame(
33			BagOStuff::QOS_LOCALITY_PROC,
34			$bag->getQoS( BagOStuff::ATTR_LOCALITY )
35		);
36	}
37
38	/**
39	 * @covers HashBagOStuff::__construct
40	 */
41	public function testConstructBadZero() {
42		$this->expectException( InvalidArgumentException::class );
43		$cache = new HashBagOStuff( [ 'maxKeys' => 0 ] );
44	}
45
46	/**
47	 * @covers HashBagOStuff::__construct
48	 */
49	public function testConstructBadNeg() {
50		$this->expectException( InvalidArgumentException::class );
51		$cache = new HashBagOStuff( [ 'maxKeys' => -1 ] );
52	}
53
54	/**
55	 * @covers HashBagOStuff::__construct
56	 */
57	public function testConstructBadType() {
58		$this->expectException( InvalidArgumentException::class );
59		$cache = new HashBagOStuff( [ 'maxKeys' => 'x' ] );
60	}
61
62	/**
63	 * @covers HashBagOStuff::delete
64	 */
65	public function testDelete() {
66		$cache = new HashBagOStuff();
67		for ( $i = 0; $i < 10; $i++ ) {
68			$cache->set( "key$i", 1 );
69			$this->assertSame( 1, $cache->get( "key$i" ) );
70			$cache->delete( "key$i" );
71			$this->assertFalse( $cache->get( "key$i" ) );
72		}
73	}
74
75	/**
76	 * @covers HashBagOStuff::clear
77	 */
78	public function testClear() {
79		$cache = new HashBagOStuff();
80		for ( $i = 0; $i < 10; $i++ ) {
81			$cache->set( "key$i", 1 );
82			$this->assertSame( 1, $cache->get( "key$i" ) );
83		}
84		$cache->clear();
85		for ( $i = 0; $i < 10; $i++ ) {
86			$this->assertFalse( $cache->get( "key$i" ) );
87		}
88	}
89
90	/**
91	 * @covers HashBagOStuff::doGet
92	 * @covers HashBagOStuff::expire
93	 */
94	public function testExpire() {
95		$cache = new HashBagOStuff();
96		$cacheInternal = TestingAccessWrapper::newFromObject( $cache );
97		$cache->set( 'foo', 1 );
98		$cache->set( 'bar', 1, 10 );
99		$cache->set( 'baz', 1, -10 );
100
101		$this->assertSame( 0, $cacheInternal->bag['foo'][$cache::KEY_EXP], 'Indefinite' );
102		// 2 seconds tolerance
103		$this->assertEqualsWithDelta(
104			time() + 10,
105			$cacheInternal->bag['bar'][$cache::KEY_EXP],
106			2,
107			'Future'
108		);
109		$this->assertEqualsWithDelta(
110			time() - 10,
111			$cacheInternal->bag['baz'][$cache::KEY_EXP],
112			2,
113			'Past'
114		);
115
116		$this->assertSame( 1, $cache->get( 'bar' ), 'Key not expired' );
117		$this->assertFalse( $cache->get( 'baz' ), 'Key expired' );
118	}
119
120	/**
121	 * Ensure maxKeys eviction prefers keeping new keys.
122	 *
123	 * @covers HashBagOStuff::set
124	 */
125	public function testEvictionAdd() {
126		$cache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
127		for ( $i = 0; $i < 10; $i++ ) {
128			$cache->set( "key$i", 1 );
129			$this->assertSame( 1, $cache->get( "key$i" ) );
130		}
131		for ( $i = 10; $i < 20; $i++ ) {
132			$cache->set( "key$i", 1 );
133			$this->assertSame( 1, $cache->get( "key$i" ) );
134			$this->assertFalse( $cache->get( "key" . ( $i - 10 ) ) );
135		}
136	}
137
138	/**
139	 * Ensure maxKeys eviction prefers recently set keys
140	 * even if the keys pre-exist.
141	 *
142	 * @covers HashBagOStuff::set
143	 */
144	public function testEvictionSet() {
145		$cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
146
147		foreach ( [ 'foo', 'bar', 'baz' ] as $key ) {
148			$cache->set( $key, 1 );
149		}
150
151		// Set existing key
152		$cache->set( 'foo', 1 );
153
154		// Add a 4th key (beyond the allowed maximum)
155		$cache->set( 'quux', 1 );
156
157		// Foo's life should have been extended over Bar
158		foreach ( [ 'foo', 'baz', 'quux' ] as $key ) {
159			$this->assertSame( 1, $cache->get( $key ), "Kept $key" );
160		}
161		$this->assertFalse( $cache->get( 'bar' ), 'Evicted bar' );
162	}
163
164	/**
165	 * Ensure maxKeys eviction prefers recently retrieved keys (LRU).
166	 *
167	 * @covers HashBagOStuff::doGet
168	 * @covers HashBagOStuff::hasKey
169	 */
170	public function testEvictionGet() {
171		$cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
172
173		foreach ( [ 'foo', 'bar', 'baz' ] as $key ) {
174			$cache->set( $key, 1 );
175		}
176
177		// Get existing key
178		$cache->get( 'foo', 1 );
179
180		// Add a 4th key (beyond the allowed maximum)
181		$cache->set( 'quux', 1 );
182
183		// Foo's life should have been extended over Bar
184		foreach ( [ 'foo', 'baz', 'quux' ] as $key ) {
185			$this->assertSame( 1, $cache->get( $key ), "Kept $key" );
186		}
187		$this->assertFalse( $cache->get( 'bar' ), 'Evicted bar' );
188	}
189}
190