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 );
34 ChangeTags
::updateTags( [ 'foo', 'bar' ], [], $rcId );
35 // HACK resolve deferred group concats (see comment in provideModifyDisplayQuery)
36 if ( isset( $modifiedQuery['fields']['ts_tags'] ) ) {
37 $modifiedQuery['fields']['ts_tags'] = call_user_func_array(
38 [ wfGetDB( DB_REPLICA
), 'buildGroupConcatField' ],
39 $modifiedQuery['fields']['ts_tags']
42 if ( isset( $modifiedQuery['exception'] ) ) {
43 $this->setExpectedException( $modifiedQuery['exception'] );
45 ChangeTags
::modifyDisplayQuery(
49 $origQuery['join_conds'],
50 $origQuery['options'],
53 if ( !isset( $modifiedQuery['exception'] ) ) {
54 $this->assertArrayEquals(
57 /* ordered = */ false,
63 public function provideModifyDisplayQuery() {
64 // HACK if we call $dbr->buildGroupConcatField() now, it will return the wrong table names
65 // We have to have the test runner call it instead
66 $baseConcats = [ ',', [ 'change_tag', 'change_tag_def' ], 'ctd_name' ];
67 $joinConds = [ 'change_tag_def' => [ 'INNER JOIN', 'ct_tag_id=ctd_id' ] ];
69 'recentchanges' => array_merge( $baseConcats, [ 'ct_rc_id=rc_id', $joinConds ] ),
70 'logging' => array_merge( $baseConcats, [ 'ct_log_id=log_id', $joinConds ] ),
71 'revision' => array_merge( $baseConcats, [ 'ct_rev_id=rev_id', $joinConds ] ),
72 'archive' => array_merge( $baseConcats, [ 'ct_rev_id=ar_rev_id', $joinConds ] ),
76 'simple recentchanges query' => [
78 'tables' => [ 'recentchanges' ],
79 'fields' => [ 'rc_id', 'rc_timestamp' ],
80 'conds' => [ "rc_timestamp > '20170714183203'" ],
82 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
85 true, // tag filtering enabled
87 'tables' => [ 'recentchanges' ],
88 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
89 'conds' => [ "rc_timestamp > '20170714183203'" ],
91 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
94 'simple query with strings' => [
96 'tables' => 'recentchanges',
98 'conds' => "rc_timestamp > '20170714183203'",
100 'options' => 'ORDER BY rc_timestamp DESC',
103 true, // tag filtering enabled
105 'tables' => [ 'recentchanges' ],
106 'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
107 'conds' => [ "rc_timestamp > '20170714183203'" ],
109 'options' => [ 'ORDER BY rc_timestamp DESC' ],
112 'recentchanges query with single tag filter' => [
114 'tables' => [ 'recentchanges' ],
115 'fields' => [ 'rc_id', 'rc_timestamp' ],
116 'conds' => [ "rc_timestamp > '20170714183203'" ],
118 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
121 true, // tag filtering enabled
123 'tables' => [ 'recentchanges', 'change_tag' ],
124 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
125 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
126 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
127 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
130 'logging query with single tag filter and strings' => [
132 'tables' => 'logging',
133 'fields' => 'log_id',
134 'conds' => "log_timestamp > '20170714183203'",
136 'options' => 'ORDER BY log_timestamp DESC',
139 true, // tag filtering enabled
141 'tables' => [ 'logging', 'change_tag' ],
142 'fields' => [ 'log_id', 'ts_tags' => $groupConcats['logging'] ],
143 'conds' => [ "log_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
144 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id=log_id' ] ],
145 'options' => [ 'ORDER BY log_timestamp DESC' ],
148 'revision query with single tag filter' => [
150 'tables' => [ 'revision' ],
151 'fields' => [ 'rev_id', 'rev_timestamp' ],
152 'conds' => [ "rev_timestamp > '20170714183203'" ],
154 'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
157 true, // tag filtering enabled
159 'tables' => [ 'revision', 'change_tag' ],
160 'fields' => [ 'rev_id', 'rev_timestamp', 'ts_tags' => $groupConcats['revision'] ],
161 'conds' => [ "rev_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
162 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=rev_id' ] ],
163 'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
166 'archive query with single tag filter' => [
168 'tables' => [ 'archive' ],
169 'fields' => [ 'ar_id', 'ar_timestamp' ],
170 'conds' => [ "ar_timestamp > '20170714183203'" ],
172 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
175 true, // tag filtering enabled
177 'tables' => [ 'archive', 'change_tag' ],
178 'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
179 'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
180 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=ar_rev_id' ] ],
181 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
184 'unsupported table name throws exception (even without tag filter)' => [
186 'tables' => [ 'foobar' ],
187 'fields' => [ 'fb_id', 'fb_timestamp' ],
188 'conds' => [ "fb_timestamp > '20170714183203'" ],
190 'options' => [ 'ORDER BY' => 'fb_timestamp DESC' ],
193 true, // tag filtering enabled
194 [ 'exception' => MWException
::class ]
196 'tag filter ignored when tag filtering is disabled' => [
198 'tables' => [ 'archive' ],
199 'fields' => [ 'ar_id', 'ar_timestamp' ],
200 'conds' => [ "ar_timestamp > '20170714183203'" ],
202 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
205 false, // tag filtering disabled
207 'tables' => [ 'archive' ],
208 'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
209 'conds' => [ "ar_timestamp > '20170714183203'" ],
211 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
214 'recentchanges query with multiple tag filter' => [
216 'tables' => [ 'recentchanges' ],
217 'fields' => [ 'rc_id', 'rc_timestamp' ],
218 'conds' => [ "rc_timestamp > '20170714183203'" ],
220 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
223 true, // tag filtering enabled
225 'tables' => [ 'recentchanges', 'change_tag' ],
226 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
227 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
228 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
229 'options' => [ 'ORDER BY' => 'rc_timestamp DESC', 'DISTINCT' ],
232 'recentchanges query with multiple tag filter that already has DISTINCT' => [
234 'tables' => [ 'recentchanges' ],
235 'fields' => [ 'rc_id', 'rc_timestamp' ],
236 'conds' => [ "rc_timestamp > '20170714183203'" ],
238 'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
241 true, // tag filtering enabled
243 'tables' => [ 'recentchanges', 'change_tag' ],
244 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
245 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
246 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
247 'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
250 'recentchanges query with multiple tag filter with strings' => [
252 'tables' => 'recentchanges',
254 'conds' => "rc_timestamp > '20170714183203'",
256 'options' => 'ORDER BY rc_timestamp DESC',
259 true, // tag filtering enabled
261 'tables' => [ 'recentchanges', 'change_tag' ],
262 'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
263 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
264 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
265 'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
271 public static function dataGetSoftwareTags() {
275 'mw-contentModelChange' => true,
276 'mw-redirect' => true,
277 'mw-rollback' => true,
290 'mw-contentmodelchanged' => true,
291 'mw-replace' => true,
292 'mw-new-redirects' => true,
293 'mw-changed-redirect-target' => true,
294 'mw-rolback' => true,
295 'mw-blanking' => false
299 'mw-changed-redirect-target'
321 * @dataProvider dataGetSoftwareTags
322 * @covers ChangeTags::getSoftwareTags
324 public function testGetSoftwareTags( $softwareTags, $expected ) {
325 $this->setMwGlobals( 'wgSoftwareTags', $softwareTags );
327 $actual = ChangeTags
::getSoftwareTags();
328 // Order of tags in arrays is not important
331 $this->assertEquals( $expected, $actual );
334 public function testUpdateTagsMigrationOld() {
335 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
336 $dbw = wfGetDB( DB_MASTER
);
337 $dbw->delete( 'change_tag', '*' );
338 $dbw->delete( 'change_tag_def', '*' );
341 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
343 $dbr = wfGetDB( DB_REPLICA
);
345 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
346 $this->assertEquals( [], iterator_to_array( $res, false ) );
360 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
361 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
364 ChangeTags
::updateTags( [ 'tag1', 'tag3' ], [], $rcId );
366 $dbr = wfGetDB( DB_REPLICA
);
368 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
369 $this->assertEquals( [], iterator_to_array( $res, false ) );
393 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
394 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
397 public function testUpdateTagsMigrationWriteBoth() {
398 // FIXME: fails under postgres
399 $this->markTestSkippedIfDbType( 'postgres' );
401 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
402 $dbw = wfGetDB( DB_MASTER
);
403 $dbw->delete( 'change_tag', '*' );
404 $dbw->delete( 'change_tag_def', '*' );
407 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
409 $dbr = wfGetDB( DB_REPLICA
);
413 'ctd_name' => 'tag1',
418 'ctd_name' => 'tag2',
423 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
424 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
438 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
439 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
442 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
444 ChangeTags
::updateTags( [ 'tag3' ], [], $rcId );
446 $dbr = wfGetDB( DB_REPLICA
);
450 'ctd_name' => 'tag1',
455 'ctd_name' => 'tag2',
460 'ctd_name' => 'tag3',
465 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
466 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
490 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
491 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
494 public function testDeleteTagsMigrationOld() {
495 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
496 $dbw = wfGetDB( DB_MASTER
);
497 $dbw->delete( 'change_tag', '*' );
498 $dbw->delete( 'change_tag_def', '*' );
501 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
503 ChangeTags
::updateTags( [], [ 'tag2' ], $rcId );
505 $dbr = wfGetDB( DB_REPLICA
);
507 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
508 $this->assertEquals( [], iterator_to_array( $res, false ) );
517 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
518 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
521 public function testDeleteTagsMigrationWriteBoth() {
522 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
523 $dbw = wfGetDB( DB_MASTER
);
524 $dbw->delete( 'change_tag', '*' );
525 $dbw->delete( 'change_tag_def', '*' );
526 MediaWikiServices
::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
529 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
531 ChangeTags
::updateTags( [], [ 'tag2' ], $rcId );
533 $dbr = wfGetDB( DB_REPLICA
);
537 'ctd_name' => 'tag1',
542 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
543 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
552 $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' );
553 $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
556 public function testTagUsageStatisticsOldBackend() {
557 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
558 $this->setMwGlobals( 'wgTagStatisticsNewTable', false );
560 $dbw = wfGetDB( DB_MASTER
);
561 $dbw->delete( 'change_tag', '*' );
562 $dbw->delete( 'change_tag_def', '*' );
565 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
568 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
570 $this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags
::tagUsageStatistics() );
573 public function testTagUsageStatisticsNewMigrationOldBackedn() {
574 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
575 $this->setMwGlobals( 'wgTagStatisticsNewTable', false );
577 $dbw = wfGetDB( DB_MASTER
);
578 $dbw->delete( 'change_tag', '*' );
579 $dbw->delete( 'change_tag_def', '*' );
580 MediaWikiServices
::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
583 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
586 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
588 $this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags
::tagUsageStatistics() );
591 public function testTagUsageStatisticsNewBackend() {
592 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
593 $this->setMwGlobals( 'wgTagStatisticsNewTable', true );
595 $dbw = wfGetDB( DB_MASTER
);
596 $dbw->delete( 'change_tag', '*' );
597 $dbw->delete( 'change_tag_def', '*' );
598 MediaWikiServices
::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
601 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
604 ChangeTags
::updateTags( [ 'tag1' ], [], $rcId );
606 $this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags
::tagUsageStatistics() );
609 public function testListExplicitlyDefinedTagsOld() {
610 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD
);
611 $dbw = wfGetDB( DB_MASTER
);
612 $dbw->delete( 'change_tag', '*' );
613 $dbw->delete( 'change_tag_def', '*' );
614 $dbw->delete( 'valid_tag', '*' );
617 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
618 ChangeTags
::defineTag( 'tag2' );
620 $this->assertEquals( [ 'tag2' ], ChangeTags
::listExplicitlyDefinedTags() );
621 $dbr = wfGetDB( DB_REPLICA
);
622 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
623 $this->assertEquals( [], iterator_to_array( $res, false ) );
625 $this->assertEquals( [ 'tag2' ], $dbr->selectFieldValues( 'valid_tag', 'vt_tag', '' ) );
628 public function testListExplicitlyDefinedTagsWriteBoth() {
629 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
630 $dbw = wfGetDB( DB_MASTER
);
631 $dbw->delete( 'change_tag', '*' );
632 $dbw->delete( 'change_tag_def', '*' );
633 $dbw->delete( 'valid_tag', '*' );
636 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
637 ChangeTags
::defineTag( 'tag2' );
639 $this->assertEquals( [ 'tag2' ], ChangeTags
::listExplicitlyDefinedTags() );
640 $dbr = wfGetDB( DB_REPLICA
);
644 'ctd_name' => 'tag1',
645 'ctd_user_defined' => 0
648 'ctd_name' => 'tag2',
649 'ctd_user_defined' => 1
652 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
653 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
655 $this->assertEquals( [ 'tag2' ], $dbr->selectFieldValues( 'valid_tag', 'vt_tag', '' ) );
658 public function testListExplicitlyDefinedTagsNew() {
659 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_NEW
);
660 $dbw = wfGetDB( DB_MASTER
);
661 $dbw->delete( 'change_tag', '*' );
662 $dbw->delete( 'change_tag_def', '*' );
663 $dbw->delete( 'valid_tag', '*' );
666 ChangeTags
::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
667 ChangeTags
::defineTag( 'tag2' );
669 $this->assertEquals( [ 'tag2' ], ChangeTags
::listExplicitlyDefinedTags() );
670 $dbr = wfGetDB( DB_REPLICA
);
674 'ctd_name' => 'tag1',
675 'ctd_user_defined' => 0
678 'ctd_name' => 'tag2',
679 'ctd_user_defined' => 1
682 $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
683 $this->assertEquals( $expected, iterator_to_array( $res, false ) );
685 $this->assertEquals( [], $dbr->selectFieldValues( 'valid_tag', 'vt_tag', '' ) );