3 use MediaWiki\MediaWikiServices
;
9 class ChangeTagsTest
extends MediaWikiTestCase
{
11 public function setUp() {
14 $this->tablesUsed
[] = 'change_tag';
15 $this->tablesUsed
[] = 'change_tag_def';
16 $this->tablesUsed
[] = 'tag_summary';
17 $this->tablesUsed
[] = 'valid_tag';
19 // Truncate these to avoid the supposed-to-be-unused IDs in tests here turning
20 // out to be used, leading ChangeTags::updateTags() to pick up bogus rc_id,
21 // log_id, or rev_id values and run into unique constraint violations.
22 $this->tablesUsed
[] = 'recentchanges';
23 $this->tablesUsed
[] = 'logging';
24 $this->tablesUsed
[] = 'revision';
25 $this->tablesUsed
[] = 'archive';
28 // TODO only modifyDisplayQuery and getSoftwareTags are tested, nothing else is
30 /** @dataProvider provideModifyDisplayQuery */
31 public function testModifyDisplayQuery( $origQuery, $filter_tag, $useTags, $modifiedQuery ) {
32 $this->setMwGlobals( 'wgUseTagFilter', $useTags );
33 // HACK resolve deferred group concats (see comment in provideModifyDisplayQuery)
34 if ( isset( $modifiedQuery['fields']['ts_tags'] ) ) {
35 $modifiedQuery['fields']['ts_tags'] = call_user_func_array(
36 [ wfGetDB( DB_REPLICA
), 'buildGroupConcatField' ],
37 $modifiedQuery['fields']['ts_tags']
40 if ( isset( $modifiedQuery['exception'] ) ) {
41 $this->setExpectedException( $modifiedQuery['exception'] );
43 ChangeTags
::modifyDisplayQuery(
47 $origQuery['join_conds'],
48 $origQuery['options'],
51 if ( !isset( $modifiedQuery['exception'] ) ) {
52 $this->assertArrayEquals(
55 /* ordered = */ false,
61 public function provideModifyDisplayQuery() {
62 // HACK if we call $dbr->buildGroupConcatField() now, it will return the wrong table names
63 // We have to have the test runner call it instead
65 'recentchanges' => [ ',', 'change_tag', 'ct_tag', 'ct_rc_id=rc_id' ],
66 'logging' => [ ',', 'change_tag', 'ct_tag', 'ct_log_id=log_id' ],
67 'revision' => [ ',', 'change_tag', 'ct_tag', 'ct_rev_id=rev_id' ],
68 'archive' => [ ',', 'change_tag', 'ct_tag', 'ct_rev_id=ar_rev_id' ],
72 'simple recentchanges query' => [
74 'tables' => [ 'recentchanges' ],
75 'fields' => [ 'rc_id', 'rc_timestamp' ],
76 'conds' => [ "rc_timestamp > '20170714183203'" ],
78 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
81 true, // tag filtering enabled
83 'tables' => [ 'recentchanges' ],
84 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
85 'conds' => [ "rc_timestamp > '20170714183203'" ],
87 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
90 'simple query with strings' => [
92 'tables' => 'recentchanges',
94 'conds' => "rc_timestamp > '20170714183203'",
96 'options' => 'ORDER BY rc_timestamp DESC',
99 true, // tag filtering enabled
101 'tables' => [ 'recentchanges' ],
102 'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
103 'conds' => [ "rc_timestamp > '20170714183203'" ],
105 'options' => [ 'ORDER BY rc_timestamp DESC' ],
108 'recentchanges query with single tag filter' => [
110 'tables' => [ 'recentchanges' ],
111 'fields' => [ 'rc_id', 'rc_timestamp' ],
112 'conds' => [ "rc_timestamp > '20170714183203'" ],
114 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
117 true, // tag filtering enabled
119 'tables' => [ 'recentchanges', 'change_tag' ],
120 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
121 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag' => 'foo' ],
122 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
123 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
126 'logging query with single tag filter and strings' => [
128 'tables' => 'logging',
129 'fields' => 'log_id',
130 'conds' => "log_timestamp > '20170714183203'",
132 'options' => 'ORDER BY log_timestamp DESC',
135 true, // tag filtering enabled
137 'tables' => [ 'logging', 'change_tag' ],
138 'fields' => [ 'log_id', 'ts_tags' => $groupConcats['logging'] ],
139 'conds' => [ "log_timestamp > '20170714183203'", 'ct_tag' => 'foo' ],
140 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id=log_id' ] ],
141 'options' => [ 'ORDER BY log_timestamp DESC' ],
144 'revision query with single tag filter' => [
146 'tables' => [ 'revision' ],
147 'fields' => [ 'rev_id', 'rev_timestamp' ],
148 'conds' => [ "rev_timestamp > '20170714183203'" ],
150 'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
153 true, // tag filtering enabled
155 'tables' => [ 'revision', 'change_tag' ],
156 'fields' => [ 'rev_id', 'rev_timestamp', 'ts_tags' => $groupConcats['revision'] ],
157 'conds' => [ "rev_timestamp > '20170714183203'", 'ct_tag' => 'foo' ],
158 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=rev_id' ] ],
159 'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
162 'archive query with single tag filter' => [
164 'tables' => [ 'archive' ],
165 'fields' => [ 'ar_id', 'ar_timestamp' ],
166 'conds' => [ "ar_timestamp > '20170714183203'" ],
168 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
171 true, // tag filtering enabled
173 'tables' => [ 'archive', 'change_tag' ],
174 'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
175 'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag' => 'foo' ],
176 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=ar_rev_id' ] ],
177 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
180 'unsupported table name throws exception (even without tag filter)' => [
182 'tables' => [ 'foobar' ],
183 'fields' => [ 'fb_id', 'fb_timestamp' ],
184 'conds' => [ "fb_timestamp > '20170714183203'" ],
186 'options' => [ 'ORDER BY' => 'fb_timestamp DESC' ],
189 true, // tag filtering enabled
190 [ 'exception' => MWException
::class ]
192 'tag filter ignored when tag filtering is disabled' => [
194 'tables' => [ 'archive' ],
195 'fields' => [ 'ar_id', 'ar_timestamp' ],
196 'conds' => [ "ar_timestamp > '20170714183203'" ],
198 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
201 false, // tag filtering disabled
203 'tables' => [ 'archive' ],
204 'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
205 'conds' => [ "ar_timestamp > '20170714183203'" ],
207 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
210 'recentchanges query with multiple tag filter' => [
212 'tables' => [ 'recentchanges' ],
213 'fields' => [ 'rc_id', 'rc_timestamp' ],
214 'conds' => [ "rc_timestamp > '20170714183203'" ],
216 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
219 true, // tag filtering enabled
221 'tables' => [ 'recentchanges', 'change_tag' ],
222 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
223 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag' => [ 'foo', 'bar' ] ],
224 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
225 'options' => [ 'ORDER BY' => 'rc_timestamp DESC', 'DISTINCT' ],
228 'recentchanges query with multiple tag filter that already has DISTINCT' => [
230 'tables' => [ 'recentchanges' ],
231 'fields' => [ 'rc_id', 'rc_timestamp' ],
232 'conds' => [ "rc_timestamp > '20170714183203'" ],
234 'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
237 true, // tag filtering enabled
239 'tables' => [ 'recentchanges', 'change_tag' ],
240 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
241 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag' => [ 'foo', 'bar' ] ],
242 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
243 'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
246 'recentchanges query with multiple tag filter with strings' => [
248 'tables' => 'recentchanges',
250 'conds' => "rc_timestamp > '20170714183203'",
252 'options' => 'ORDER BY rc_timestamp DESC',
255 true, // tag filtering enabled
257 'tables' => [ 'recentchanges', 'change_tag' ],
258 'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
259 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag' => [ 'foo', 'bar' ] ],
260 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
261 'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
267 public static function dataGetSoftwareTags() {
271 'mw-contentModelChange' => true,
272 'mw-redirect' => true,
273 'mw-rollback' => true,
286 'mw-contentmodelchanged' => true,
287 'mw-replace' => true,
288 'mw-new-redirects' => true,
289 'mw-changed-redirect-target' => true,
290 'mw-rolback' => true,
291 'mw-blanking' => false
295 'mw-changed-redirect-target'
317 * @dataProvider dataGetSoftwareTags
318 * @covers ChangeTags::getSoftwareTags
320 public function testGetSoftwareTags( $softwareTags, $expected ) {
321 $this->setMwGlobals( 'wgSoftwareTags', $softwareTags );
323 $actual = ChangeTags
::getSoftwareTags();
324 // Order of tags in arrays is not important
327 $this->assertEquals( $expected, $actual );
330 public function testUpdateTagsMigrationOld() {
331 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
332 $dbw = wfGetDB( DB_MASTER
);
333 $dbw->delete( 'change_tag', '*' );
334 $dbw->delete( 'change_tag_def', '*' );
337 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
339 $dbr = wfGetDB( DB_REPLICA
);
341 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
342 $this->assertEquals( [], iterator_to_array( $res, false ) );
356 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
357 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
360 ChangeTags
::updateTags( [ 'tag1', 'tag3' ], [], $rcId );
362 $dbr = wfGetDB( DB_REPLICA
);
364 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
365 $this->assertEquals( [], iterator_to_array( $res, false ) );
389 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
390 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
393 public function testUpdateTagsMigrationWriteBoth() {
394 // FIXME: fails under postgres
395 $this->markTestSkippedIfDbType( 'postgres' );
397 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
398 $dbw = wfGetDB( DB_MASTER
);
399 $dbw->delete( 'change_tag', '*' );
400 $dbw->delete( 'change_tag_def', '*' );
403 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
405 $dbr = wfGetDB( DB_REPLICA
);
409 'ctd_name' => 'tag1',
414 'ctd_name' => 'tag2',
419 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
420 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
434 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
435 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
438 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
440 ChangeTags
::updateTags( [ 'tag3' ], [], $rcId );
442 $dbr = wfGetDB( DB_REPLICA
);
446 'ctd_name' => 'tag1',
451 'ctd_name' => 'tag2',
456 'ctd_name' => 'tag3',
461 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
462 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
486 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
487 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
490 public function testDeleteTagsMigrationOld() {
491 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
492 $dbw = wfGetDB( DB_MASTER
);
493 $dbw->delete( 'change_tag', '*' );
494 $dbw->delete( 'change_tag_def', '*' );
497 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
499 ChangeTags
::updateTags( [], [ 'tag2' ], $rcId );
501 $dbr = wfGetDB( DB_REPLICA
);
503 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
504 $this->assertEquals( [], iterator_to_array( $res, false ) );
513 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
514 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
517 public function testDeleteTagsMigrationWriteBoth() {
518 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
519 $dbw = wfGetDB( DB_MASTER
);
520 $dbw->delete( 'change_tag', '*' );
521 $dbw->delete( 'change_tag_def', '*' );
522 MediaWikiServices
::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
525 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
527 ChangeTags
::updateTags( [], [ 'tag2' ], $rcId );
529 $dbr = wfGetDB( DB_REPLICA
);
533 'ctd_name' => 'tag1',
538 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
539 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
548 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
549 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
552 public function testTagUsageStatisticsOldBackend() {
553 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
554 $this->setMwGlobals( 'wgTagStatisticsNewTable', false );
556 $dbw = wfGetDB( DB_MASTER
);
557 $dbw->delete( 'change_tag', '*' );
558 $dbw->delete( 'change_tag_def', '*' );
561 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
564 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
566 $this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags
::tagUsageStatistics() );
569 public function testTagUsageStatisticsNewMigrationOldBackedn() {
570 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
571 $this->setMwGlobals( 'wgTagStatisticsNewTable', false );
573 $dbw = wfGetDB( DB_MASTER
);
574 $dbw->delete( 'change_tag', '*' );
575 $dbw->delete( 'change_tag_def', '*' );
576 MediaWikiServices
::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
579 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
582 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
584 $this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags
::tagUsageStatistics() );
587 public function testTagUsageStatisticsNewBackend() {
588 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
589 $this->setMwGlobals( 'wgTagStatisticsNewTable', true );
591 $dbw = wfGetDB( DB_MASTER
);
592 $dbw->delete( 'change_tag', '*' );
593 $dbw->delete( 'change_tag_def', '*' );
594 MediaWikiServices
::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
597 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
600 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
602 $this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags
::tagUsageStatistics() );
605 public function testListExplicitlyDefinedTagsOld() {
606 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
607 $dbw = wfGetDB( DB_MASTER
);
608 $dbw->delete( 'change_tag', '*' );
609 $dbw->delete( 'change_tag_def', '*' );
610 $dbw->delete( 'valid_tag', '*' );
613 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
614 ChangeTags
::defineTag( 'tag2' );
616 $this->assertEquals( [ 'tag2' ], ChangeTags
::listExplicitlyDefinedTags() );
617 $dbr = wfGetDB( DB_REPLICA
);
618 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
619 $this->assertEquals( [], iterator_to_array( $res, false ) );
621 $this->assertEquals( [ 'tag2' ], $dbr->selectFieldValues( 'valid_tag', 'vt_tag', '' ) );
624 public function testListExplicitlyDefinedTagsWriteBoth() {
625 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
626 $dbw = wfGetDB( DB_MASTER
);
627 $dbw->delete( 'change_tag', '*' );
628 $dbw->delete( 'change_tag_def', '*' );
629 $dbw->delete( 'valid_tag', '*' );
632 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
633 ChangeTags
::defineTag( 'tag2' );
635 $this->assertEquals( [ 'tag2' ], ChangeTags
::listExplicitlyDefinedTags() );
636 $dbr = wfGetDB( DB_REPLICA
);
640 'ctd_name' => 'tag1',
641 'ctd_user_defined' => 0
644 'ctd_name' => 'tag2',
645 'ctd_user_defined' => 1
648 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
649 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
651 $this->assertEquals( [ 'tag2' ], $dbr->selectFieldValues( 'valid_tag', 'vt_tag', '' ) );
654 public function testListExplicitlyDefinedTagsNew() {
655 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_NEW
);
656 $dbw = wfGetDB( DB_MASTER
);
657 $dbw->delete( 'change_tag', '*' );
658 $dbw->delete( 'change_tag_def', '*' );
659 $dbw->delete( 'valid_tag', '*' );
662 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
663 ChangeTags
::defineTag( 'tag2' );
665 $this->assertEquals( [ 'tag2' ], ChangeTags
::listExplicitlyDefinedTags() );
666 $dbr = wfGetDB( DB_REPLICA
);
670 'ctd_name' => 'tag1',
671 'ctd_user_defined' => 0
674 'ctd_name' => 'tag2',
675 'ctd_user_defined' => 1
678 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
679 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
681 $this->assertEquals( [], $dbr->selectFieldValues( 'valid_tag', 'vt_tag', '' ) );