1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * @covers ChangeTags
7 * @group Database
8 */
9class ChangeTagsTest extends MediaWikiIntegrationTestCase {
10
11	protected function setUp(): void {
12		parent::setUp();
13
14		$this->tablesUsed[] = 'change_tag';
15		$this->tablesUsed[] = 'change_tag_def';
16
17		// Truncate these to avoid the supposed-to-be-unused IDs in tests here turning
18		// out to be used, leading ChangeTags::updateTags() to pick up bogus rc_id,
19		// log_id, or rev_id values and run into unique constraint violations.
20		$this->tablesUsed[] = 'recentchanges';
21		$this->tablesUsed[] = 'logging';
22		$this->tablesUsed[] = 'revision';
23		$this->tablesUsed[] = 'archive';
24	}
25
26	public function tearDown(): void {
27		ChangeTags::$avoidReopeningTablesForTesting = false;
28		parent::tearDown();
29	}
30
31	private function emptyChangeTagsTables() {
32		$dbw = wfGetDB( DB_PRIMARY );
33		$dbw->delete( 'change_tag', '*' );
34		$dbw->delete( 'change_tag_def', '*' );
35	}
36
37	// TODO most methods are not tested
38
39	/** @dataProvider provideModifyDisplayQuery */
40	public function testModifyDisplayQuery(
41		$origQuery,
42		$filter_tag,
43		$useTags,
44		$avoidReopeningTables,
45		$modifiedQuery
46	) {
47		$this->setMwGlobals( 'wgUseTagFilter', $useTags );
48
49		if ( $avoidReopeningTables && $this->db->getType() !== 'mysql' ) {
50			$this->markTestSkipped( 'MySQL only' );
51		}
52
53		ChangeTags::$avoidReopeningTablesForTesting = $avoidReopeningTables;
54
55		$rcId = 123;
56		ChangeTags::updateTags( [ 'foo', 'bar' ], [], $rcId );
57		// HACK resolve deferred group concats (see comment in provideModifyDisplayQuery)
58		if ( isset( $modifiedQuery['fields']['ts_tags'] ) ) {
59			$modifiedQuery['fields']['ts_tags'] = wfGetDB( DB_REPLICA )
60				->buildGroupConcatField( ...$modifiedQuery['fields']['ts_tags'] );
61		}
62		if ( isset( $modifiedQuery['exception'] ) ) {
63			$this->expectException( $modifiedQuery['exception'] );
64		}
65		ChangeTags::modifyDisplayQuery(
66			$origQuery['tables'],
67			$origQuery['fields'],
68			$origQuery['conds'],
69			$origQuery['join_conds'],
70			$origQuery['options'],
71			$filter_tag
72		);
73		if ( !isset( $modifiedQuery['exception'] ) ) {
74			$this->assertArrayEquals(
75				$modifiedQuery,
76				$origQuery,
77				/* ordered = */ false,
78				/* named = */ true
79			);
80		}
81	}
82
83	public function provideModifyDisplayQuery() {
84		// HACK if we call $dbr->buildGroupConcatField() now, it will return the wrong table names
85		// We have to have the test runner call it instead
86		$baseConcats = [ ',', [ 'change_tag', 'change_tag_def' ], 'ctd_name' ];
87		$joinConds = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
88		$groupConcats = [
89			'recentchanges' => array_merge( $baseConcats, [ 'ct_rc_id=rc_id', $joinConds ] ),
90			'logging' => array_merge( $baseConcats, [ 'ct_log_id=log_id', $joinConds ] ),
91			'revision' => array_merge( $baseConcats, [ 'ct_rev_id=rev_id', $joinConds ] ),
92			'archive' => array_merge( $baseConcats, [ 'ct_rev_id=ar_rev_id', $joinConds ] ),
93		];
94
95		return [
96			'simple recentchanges query' => [
97				[
98					'tables' => [ 'recentchanges' ],
99					'fields' => [ 'rc_id', 'rc_timestamp' ],
100					'conds' => [ "rc_timestamp > '20170714183203'" ],
101					'join_conds' => [],
102					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
103				],
104				'', // no tag filter
105				true, // tag filtering enabled
106				false, // not avoiding reopening tables
107				[
108					'tables' => [ 'recentchanges' ],
109					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
110					'conds' => [ "rc_timestamp > '20170714183203'" ],
111					'join_conds' => [],
112					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
113				]
114			],
115			'simple query with strings' => [
116				[
117					'tables' => 'recentchanges',
118					'fields' => 'rc_id',
119					'conds' => "rc_timestamp > '20170714183203'",
120					'join_conds' => [],
121					'options' => 'ORDER BY rc_timestamp DESC',
122				],
123				'', // no tag filter
124				true, // tag filtering enabled
125				false, // not avoiding reopening tables
126				[
127					'tables' => [ 'recentchanges' ],
128					'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
129					'conds' => [ "rc_timestamp > '20170714183203'" ],
130					'join_conds' => [],
131					'options' => [ 'ORDER BY rc_timestamp DESC' ],
132				]
133			],
134			'recentchanges query with single tag filter' => [
135				[
136					'tables' => [ 'recentchanges' ],
137					'fields' => [ 'rc_id', 'rc_timestamp' ],
138					'conds' => [ "rc_timestamp > '20170714183203'" ],
139					'join_conds' => [],
140					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
141				],
142				'foo',
143				true, // tag filtering enabled
144				false, // not avoiding reopening tables
145				[
146					'tables' => [ 'recentchanges', 'change_tag' ],
147					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
148					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
149					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
150					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
151				]
152			],
153			'logging query with single tag filter and strings' => [
154				[
155					'tables' => 'logging',
156					'fields' => 'log_id',
157					'conds' => "log_timestamp > '20170714183203'",
158					'join_conds' => [],
159					'options' => 'ORDER BY log_timestamp DESC',
160				],
161				'foo',
162				true, // tag filtering enabled
163				false, // not avoiding reopening tables
164				[
165					'tables' => [ 'logging', 'change_tag' ],
166					'fields' => [ 'log_id', 'ts_tags' => $groupConcats['logging'] ],
167					'conds' => [ "log_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
168					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_log_id=log_id' ] ],
169					'options' => [ 'ORDER BY log_timestamp DESC' ],
170				]
171			],
172			'revision query with single tag filter' => [
173				[
174					'tables' => [ 'revision' ],
175					'fields' => [ 'rev_id', 'rev_timestamp' ],
176					'conds' => [ "rev_timestamp > '20170714183203'" ],
177					'join_conds' => [],
178					'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
179				],
180				'foo',
181				true, // tag filtering enabled
182				false, // not avoiding reopening tables
183				[
184					'tables' => [ 'revision', 'change_tag' ],
185					'fields' => [ 'rev_id', 'rev_timestamp', 'ts_tags' => $groupConcats['revision'] ],
186					'conds' => [ "rev_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
187					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=rev_id' ] ],
188					'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
189				]
190			],
191			'archive query with single tag filter' => [
192				[
193					'tables' => [ 'archive' ],
194					'fields' => [ 'ar_id', 'ar_timestamp' ],
195					'conds' => [ "ar_timestamp > '20170714183203'" ],
196					'join_conds' => [],
197					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
198				],
199				'foo',
200				true, // tag filtering enabled
201				false, // not avoiding reopening tables
202				[
203					'tables' => [ 'archive', 'change_tag' ],
204					'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
205					'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
206					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=ar_rev_id' ] ],
207					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
208				]
209			],
210			'archive query with single tag filter, avoiding reopening tables' => [
211				[
212					'tables' => [ 'archive' ],
213					'fields' => [ 'ar_id', 'ar_timestamp' ],
214					'conds' => [ "ar_timestamp > '20170714183203'" ],
215					'join_conds' => [],
216					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
217				],
218				'foo',
219				true, // tag filtering enabled
220				true, // avoid reopening tables
221				[
222					'tables' => [ 'archive', 'change_tag_for_display_query' ],
223					'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
224					'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
225					'join_conds' => [ 'change_tag_for_display_query' => [ 'JOIN', 'ct_rev_id=ar_rev_id' ] ],
226					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
227				]
228			],
229			'unsupported table name throws exception (even without tag filter)' => [
230				[
231					'tables' => [ 'foobar' ],
232					'fields' => [ 'fb_id', 'fb_timestamp' ],
233					'conds' => [ "fb_timestamp > '20170714183203'" ],
234					'join_conds' => [],
235					'options' => [ 'ORDER BY' => 'fb_timestamp DESC' ],
236				],
237				'',
238				true, // tag filtering enabled
239				false, // not avoiding reopening tables
240				[ 'exception' => MWException::class ]
241			],
242			'tag filter ignored when tag filtering is disabled' => [
243				[
244					'tables' => [ 'archive' ],
245					'fields' => [ 'ar_id', 'ar_timestamp' ],
246					'conds' => [ "ar_timestamp > '20170714183203'" ],
247					'join_conds' => [],
248					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
249				],
250				'foo',
251				false, // tag filtering disabled
252				false, // not avoiding reopening tables
253				[
254					'tables' => [ 'archive' ],
255					'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
256					'conds' => [ "ar_timestamp > '20170714183203'" ],
257					'join_conds' => [],
258					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
259				]
260			],
261			'recentchanges query with multiple tag filter' => [
262				[
263					'tables' => [ 'recentchanges' ],
264					'fields' => [ 'rc_id', 'rc_timestamp' ],
265					'conds' => [ "rc_timestamp > '20170714183203'" ],
266					'join_conds' => [],
267					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
268				],
269				[ 'foo', 'bar' ],
270				true, // tag filtering enabled
271				false, // not avoiding reopening tables
272				[
273					'tables' => [ 'recentchanges', 'change_tag' ],
274					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
275					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
276					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
277					'options' => [ 'ORDER BY' => 'rc_timestamp DESC', 'DISTINCT' ],
278				]
279			],
280			'recentchanges query with multiple tag filter that already has DISTINCT' => [
281				[
282					'tables' => [ 'recentchanges' ],
283					'fields' => [ 'rc_id', 'rc_timestamp' ],
284					'conds' => [ "rc_timestamp > '20170714183203'" ],
285					'join_conds' => [],
286					'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
287				],
288				[ 'foo', 'bar' ],
289				true, // tag filtering enabled
290				false, // not avoiding reopening tables
291				[
292					'tables' => [ 'recentchanges', 'change_tag' ],
293					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
294					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
295					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
296					'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
297				]
298			],
299			'recentchanges query with multiple tag filter with strings' => [
300				[
301					'tables' => 'recentchanges',
302					'fields' => 'rc_id',
303					'conds' => "rc_timestamp > '20170714183203'",
304					'join_conds' => [],
305					'options' => 'ORDER BY rc_timestamp DESC',
306				],
307				[ 'foo', 'bar' ],
308				true, // tag filtering enabled
309				false, // not avoiding reopening tables
310				[
311					'tables' => [ 'recentchanges', 'change_tag' ],
312					'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
313					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
314					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
315					'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
316				]
317			],
318			'recentchanges query with multiple tag filter with strings, avoiding reopening tables' => [
319				[
320					'tables' => 'recentchanges',
321					'fields' => 'rc_id',
322					'conds' => "rc_timestamp > '20170714183203'",
323					'join_conds' => [],
324					'options' => 'ORDER BY rc_timestamp DESC',
325				],
326				[ 'foo', 'bar' ],
327				true, // tag filtering enabled
328				true, // avoid reopening tables
329				[
330					'tables' => [ 'recentchanges', 'change_tag_for_display_query' ],
331					'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
332					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
333					'join_conds' => [ 'change_tag_for_display_query' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
334					'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
335				]
336			],
337		];
338	}
339
340	public static function dataGetSoftwareTags() {
341		return [
342			[
343				[
344					'mw-contentModelChange' => true,
345					'mw-redirect' => true,
346					'mw-rollback' => true,
347					'mw-blank' => true,
348					'mw-replace' => true,
349					'mw-add-media' => true,
350				],
351				[
352					'mw-rollback',
353					'mw-replace',
354					'mw-blank',
355					'mw-add-media',
356				]
357			],
358
359			[
360				[
361					'mw-contentmodelchanged' => true,
362					'mw-replace' => true,
363					'mw-new-redirects' => true,
364					'mw-changed-redirect-target' => true,
365					'mw-rolback' => true,
366					'mw-blanking' => false
367				],
368				[
369					'mw-replace',
370					'mw-changed-redirect-target'
371				]
372			],
373
374			[
375				[
376					null,
377					false,
378					'Lorem ipsum',
379					'mw-translation'
380				],
381				[]
382			],
383
384			[
385				[],
386				[]
387			]
388		];
389	}
390
391	/**
392	 * @dataProvider dataGetSoftwareTags
393	 * @covers ChangeTags::getSoftwareTags
394	 */
395	public function testGetSoftwareTags( $softwareTags, $expected ) {
396		$this->setMwGlobals( 'wgSoftwareTags', $softwareTags );
397
398		$actual = ChangeTags::getSoftwareTags();
399		// Order of tags in arrays is not important
400		sort( $expected );
401		sort( $actual );
402		$this->assertEquals( $expected, $actual );
403	}
404
405	public function testUpdateTags() {
406		$this->emptyChangeTagsTables();
407
408		$rcId = 123;
409		$revId = 341;
410		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId, $revId );
411
412		$this->assertSelect(
413			'change_tag_def',
414			[ 'ctd_name', 'ctd_id', 'ctd_count' ],
415			'',
416			[
417				// values of fields 'ctd_name', 'ctd_id', 'ctd_count'
418				[ 'tag1', 1, 1 ],
419				[ 'tag2', 2, 1 ],
420			]
421		);
422		$this->assertSelect(
423			'change_tag',
424			[ 'ct_tag_id', 'ct_rc_id', 'ct_rev_id' ],
425			'',
426			[
427				// values of fields 'ct_tag_id', 'ct_rc_id', 'ct_rev_id'
428				[ 1, 123, 341 ],
429				[ 2, 123, 341 ],
430			]
431		);
432
433		$rcId = 124;
434		$revId = 342;
435		ChangeTags::updateTags( [ 'tag1' ], [], $rcId, $revId );
436		ChangeTags::updateTags( [ 'tag3' ], [], $rcId, $revId );
437
438		$this->assertSelect(
439			'change_tag_def',
440			[ 'ctd_name', 'ctd_id', 'ctd_count' ],
441			'',
442			[
443				// values of fields 'ctd_name', 'ctd_id', 'ctd_count'
444				[ 'tag1', 1, 2 ],
445				[ 'tag2', 2, 1 ],
446				[ 'tag3', 3, 1 ],
447			]
448		);
449		$this->assertSelect(
450			'change_tag',
451			[ 'ct_tag_id', 'ct_rc_id', 'ct_rev_id' ],
452			'',
453			[
454				// values of fields 'ct_tag_id', 'ct_rc_id', 'ct_rev_id'
455				[ 1, 123, 341 ],
456				[ 1, 124, 342 ],
457				[ 2, 123, 341 ],
458				[ 3, 124, 342 ],
459			]
460		);
461	}
462
463	public function testUpdateTagsSkipDuplicates() {
464		$this->emptyChangeTagsTables();
465
466		$rcId = 123;
467		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
468		ChangeTags::updateTags( [ 'tag2', 'tag3' ], [], $rcId );
469
470		$this->assertSelect(
471			'change_tag_def',
472			[ 'ctd_name', 'ctd_id', 'ctd_count' ],
473			'',
474			[
475				// values of fields 'ctd_name', 'ctd_id', 'ctd_count'
476				[ 'tag1', 1, 1 ],
477				[ 'tag2', 2, 1 ],
478				[ 'tag3', 3, 1 ],
479			]
480		);
481		$this->assertSelect(
482			'change_tag',
483			[ 'ct_tag_id', 'ct_rc_id' ],
484			'',
485			[
486				// values of fields 'ct_tag_id', 'ct_rc_id',
487				[ 1, 123 ],
488				[ 2, 123 ],
489				[ 3, 123 ],
490			]
491		);
492	}
493
494	public function testUpdateTagsDoNothingOnRepeatedCall() {
495		$this->emptyChangeTagsTables();
496
497		$rcId = 123;
498		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
499		$res = ChangeTags::updateTags( [ 'tag2', 'tag1' ], [], $rcId );
500		$this->assertEquals( [ [], [], [ 'tag1', 'tag2' ] ], $res );
501
502		$this->assertSelect(
503			'change_tag_def',
504			[ 'ctd_name', 'ctd_id', 'ctd_count' ],
505			'',
506			[
507				// values of fields 'ctd_name', 'ctd_id', 'ctd_count'
508				[ 'tag1', 1, 1 ],
509				[ 'tag2', 2, 1 ],
510			]
511		);
512		$this->assertSelect(
513			'change_tag',
514			[ 'ct_tag_id', 'ct_rc_id' ],
515			'',
516			[
517				// values of fields 'ct_tag_id', 'ct_rc_id',
518				[ 1, 123 ],
519				[ 2, 123 ],
520			]
521		);
522	}
523
524	public function testDeleteTags() {
525		$this->emptyChangeTagsTables();
526		MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
527
528		$rcId = 123;
529		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
530		ChangeTags::updateTags( [], [ 'tag2' ], $rcId );
531
532		$this->assertSelect(
533			'change_tag_def',
534			[ 'ctd_name', 'ctd_id', 'ctd_count' ],
535			'',
536			[
537				// values of fields 'ctd_name', 'ctd_id', 'ctd_count'
538				[ 'tag1', 1, 1 ],
539			]
540		);
541		$this->assertSelect(
542			'change_tag',
543			[ 'ct_tag_id', 'ct_rc_id' ],
544			'',
545			[
546				// values of fields 'ct_tag_id', 'ct_rc_id',
547				[ 1, 123 ],
548			]
549		);
550	}
551
552	public function provideTags() {
553		$tags = [ 'tag 1', 'tag 2', 'tag 3' ];
554		$rcId = 123;
555		$revId = 456;
556		$logId = 789;
557
558		yield [ $tags, $rcId, null, null ];
559		yield [ $tags, null, $revId, null ];
560		yield [ $tags, null, null, $logId ];
561		yield [ $tags, $rcId, $revId, null ];
562		yield [ $tags, $rcId, null, $logId ];
563		yield [ $tags, $rcId, $revId, $logId ];
564	}
565
566	/**
567	 * @dataProvider provideTags
568	 */
569	public function testGetTags( array $tags, $rcId, $revId, $logId ) {
570		ChangeTags::addTags( $tags, $rcId, $revId, $logId );
571
572		$actualTags = ChangeTags::getTags( $this->db, $rcId, $revId, $logId );
573
574		$this->assertSame( $tags, $actualTags );
575	}
576
577	public function testGetTags_multiple_arguments() {
578		$rcId = 123;
579		$revId = 456;
580		$logId = 789;
581
582		ChangeTags::addTags( [ 'tag 1' ], $rcId );
583		ChangeTags::addTags( [ 'tag 2' ], $rcId, $revId );
584		ChangeTags::addTags( [ 'tag 3' ], $rcId, $revId, $logId );
585
586		$tags3 = [ 'tag 3' ];
587		$tags2 = array_merge( $tags3, [ 'tag 2' ] );
588		$tags1 = array_merge( $tags2, [ 'tag 1' ] );
589		$this->assertArrayEquals( $tags3, ChangeTags::getTags( $this->db, $rcId, $revId, $logId ) );
590		$this->assertArrayEquals( $tags2, ChangeTags::getTags( $this->db, $rcId, $revId ) );
591		$this->assertArrayEquals( $tags1, ChangeTags::getTags( $this->db, $rcId ) );
592	}
593
594	public function testGetTagsWithData() {
595		$rcId1 = 123;
596		$rcId2 = 456;
597		$rcId3 = 789;
598		ChangeTags::addTags( [ 'tag 1' ], $rcId1, null, null, 'data1' );
599		ChangeTags::addTags( [ 'tag 3_1' ], $rcId3, null, null );
600		ChangeTags::addTags( [ 'tag 3_2' ], $rcId3, null, null, 'data3_2' );
601
602		$data = ChangeTags::getTagsWithData( $this->db, $rcId1 );
603		$this->assertSame( [ 'tag 1' => 'data1' ], $data );
604
605		$data = ChangeTags::getTagsWithData( $this->db, $rcId2 );
606		$this->assertSame( [], $data );
607
608		$data = ChangeTags::getTagsWithData( $this->db, $rcId3 );
609		$this->assertArrayEquals( [ 'tag 3_1' => null, 'tag 3_2' => 'data3_2' ], $data, false, true );
610	}
611
612	public function testTagUsageStatistics() {
613		$this->emptyChangeTagsTables();
614		MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
615
616		$rcId = 123;
617		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
618
619		$rcId = 124;
620		ChangeTags::updateTags( [ 'tag1' ], [], $rcId );
621
622		$this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags::tagUsageStatistics() );
623	}
624
625	public function testListExplicitlyDefinedTags() {
626		$this->emptyChangeTagsTables();
627
628		$rcId = 123;
629		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
630		ChangeTags::defineTag( 'tag2' );
631
632		$this->assertEquals( [ 'tag2' ], ChangeTags::listExplicitlyDefinedTags() );
633
634		$this->assertSelect(
635			'change_tag_def',
636			[ 'ctd_name', 'ctd_user_defined' ],
637			'',
638			[
639				// values of fields 'ctd_name', 'ctd_user_defined'
640				[ 'tag1', 0 ],
641				[ 'tag2', 1 ],
642			],
643			[ 'ORDER BY' => 'ctd_name' ]
644		);
645	}
646}
647