Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PopulateWithTestData
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 6
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 setupServices
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
56
 cleanupTestData
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 getRandomValueFromDistribution
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 assertOptions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3namespace MediaWiki\Extension\ReadingLists\Maintenance;
4
5use Maintenance;
6use MediaWiki\Extension\ReadingLists\ReadingListRepository;
7use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException;
8use MediaWiki\Extension\ReadingLists\Utils;
9use MediaWiki\MediaWikiServices;
10use Wikimedia\Rdbms\IDatabase;
11use Wikimedia\Rdbms\LBFactory;
12
13require_once getenv( 'MW_INSTALL_PATH' ) !== false
14    ? getenv( 'MW_INSTALL_PATH' ) . '/maintenance/Maintenance.php'
15    : __DIR__ . '/../../../maintenance/Maintenance.php';
16
17/**
18 * Fill the database with test data, or remove it.
19 */
20class PopulateWithTestData extends Maintenance {
21
22    /** @var LBFactory */
23    private $loadBalancerFactory;
24
25    /** @var IDatabase */
26    private $dbw;
27
28    public function __construct() {
29        parent::__construct();
30        $this->addDescription( 'Fill the database with test data, or remove it.' );
31        $this->addOption( 'users', 'Number of users', false, true );
32        $this->addOption( 'lists', 'Lists per user (number or stats distribution)', false, true );
33        $this->addOption( 'entries', 'Entries per list (number or stats distribution)', false, true );
34        $this->addOption( 'cleanup', 'Delete lists which look like test data' );
35        $this->requireExtension( 'ReadingLists' );
36        if ( !extension_loaded( 'stats' ) ) {
37            $this->fatalError( 'Requires the stats PHP extension' );
38        }
39    }
40
41    private function setupServices() {
42        // Can't do this in the constructor, initialization not done yet.
43        $services = MediaWikiServices::getInstance();
44        $this->loadBalancerFactory = $services->getDBLoadBalancerFactory();
45        $this->dbw = $this->loadBalancerFactory->getPrimaryDatabase( Utils::VIRTUAL_DOMAIN );
46    }
47
48    /**
49     * @inheritDoc
50     */
51    public function execute() {
52        $this->setupServices();
53        $this->assertOptions();
54        if ( $this->getOption( 'cleanup' ) ) {
55            $this->cleanupTestData();
56            return;
57        }
58
59        $projects = $this->dbw->newSelectQueryBuilder()
60            ->select( 'rlp_id' )
61            ->from( 'reading_list_project' )
62            ->caller( __METHOD__ )->fetchFieldValues();
63        if ( !$projects ) {
64            $this->fatalError( 'No projects! Please set up some' );
65        }
66        $totalLists = $totalEntries = 0;
67        stats_rand_setall( mt_rand(), mt_rand() );
68        $users = $this->getOption( 'users' );
69        for ( $i = 0; $i < $users; $i++ ) {
70            // The test data is for performance testing so we don't care whether the user exists.
71            $centralId = 1000 + $i;
72            $repository = new ReadingListRepository( $centralId, $this->loadBalancerFactory );
73            try {
74                $repository->setupForUser();
75                $i++;
76                // HACK mark default list so it will be deleted together with the rest
77                $this->dbw->newUpdateQueryBuilder()
78                    ->update( 'reading_list' )
79                    ->set( [ 'rl_description' => __FILE__ ] )
80                    ->where( [ 'rl_user_id' => $centralId, 'rl_is_default' => 1 ] )
81                    ->caller( __METHOD__ )->execute();
82            } catch ( ReadingListRepositoryException $e ) {
83                // Instead of trying to find a user ID that's not used yet, we'll be lazy
84                // and just ignore "already set up" errors.
85            }
86            $lists = $this->getRandomValueFromDistribution( $this->getOption( 'lists' ) );
87            for ( $j = 0; $j < $lists; $j++, $totalLists++ ) {
88                $list = $repository->addList( "test_$j", __FILE__ );
89                $entries = $this->getRandomValueFromDistribution( $this->getOption( 'entries' ) );
90                $rows = [];
91                for ( $k = 0; $k < $entries; $k++, $totalEntries++ ) {
92                    $project = $projects[array_rand( $projects )];
93                    // Calling addListEntry for each row separately would be a bit slow.
94                    $rows[] = [
95                        'rle_rl_id' => $list->rl_id,
96                        'rle_user_id' => $centralId,
97                        'rle_rlp_id' => $project,
98                        'rle_title' => "Test_$k",
99                    ];
100                }
101                $this->dbw->newInsertQueryBuilder()
102                    ->insertInto( 'reading_list_entry' )
103                    ->rows( $rows )
104                    ->caller( __METHOD__ )->execute();
105                $this->dbw->newUpdateQueryBuilder()
106                    ->update( 'reading_list' )
107                    ->set( [ 'rl_size' => $entries ] )
108                    ->where( [ 'rl_id' => $list->rl_id ] )
109                    ->caller( __METHOD__ )->execute();
110            }
111            $this->output( '.' );
112        }
113        $this->output( "\nAdded $totalLists lists and $totalEntries entries for $users users\n" );
114    }
115
116    private function cleanupTestData() {
117        $services = MediaWikiServices::getInstance();
118        $ids = $this->dbw->newSelectQueryBuilder()
119            ->select( 'rl_id' )
120            ->from( 'reading_list' )
121            ->where( [ 'rl_description' => __FILE__ ] )
122            ->caller( __METHOD__ )->fetchFieldValues();
123        if ( !$ids ) {
124            $this->output( "Noting to clean up\n" );
125            return;
126        }
127        $this->dbw->newDeleteQueryBuilder()
128            ->deleteFrom( 'reading_list_entry' )
129            ->where( [ 'rle_rl_id' => $ids ] )
130            ->caller( __METHOD__ )->execute();
131        $entries = $this->dbw->affectedRows();
132        $this->dbw->newDeleteQueryBuilder()
133            ->deleteFrom( 'reading_list' )
134            ->where( [ 'rl_description' => __FILE__ ] )
135            ->caller( __METHOD__ )->execute();
136        $lists = $this->dbw->affectedRows();
137        $this->output( "Deleted $lists lists and $entries entries\n" );
138    }
139
140    /**
141     * Get a random value according to some distribution. The parameter is either a constant
142     * (in which case it will be returned) or a distribution descriptor in the form of
143     * '<dist>,<param1>,<param2>,...' (no spaces) where <dist> refers to one of the stats_rand_gen_*
144     * methods (e.g. 'exponential,1' for an exponential distribution with λ=1, or 'normal,0,1' for
145     * a normal distribution with µ=0, ρ=1).
146     * The result is normalized to be a nonnegative integer.
147     * @param string $distribution
148     * @return int
149     */
150    private function getRandomValueFromDistribution( $distribution ) {
151        $params = explode( ',', $distribution );
152        $type = trim( array_shift( $params ) );
153        if ( is_numeric( $type ) ) {
154            return (int)$type;
155        }
156        $function = "stats_rand_gen_$type";
157        if (
158            !preg_match( '/[a-z_]+/', $type )
159            || !function_exists( $function )
160        ) {
161            $this->error( "invalid distribution: $distribution (could not parse '$type')" );
162        }
163        $params = array_map( function ( $param ) use ( $distribution ) {
164            if ( !is_numeric( $param ) ) {
165                $this->error( "invalid distribution: $distribution (could not parse '$param')" );
166            }
167            return (float)$param;
168        }, $params );
169        return max( (int)call_user_func_array( $function, $params ), 0 );
170    }
171
172    private function assertOptions() {
173        if ( $this->hasOption( 'cleanup' ) ) {
174            if (
175                $this->hasOption( 'users' )
176                || $this->hasOption( 'lists' )
177                || $this->hasOption( 'entries' )
178            ) {
179                $this->fatalError( "'cleanup' cannot be used together with other options" );
180            }
181        } else {
182            if (
183                !$this->hasOption( 'users' )
184                || !$this->hasOption( 'lists' )
185                || !$this->hasOption( 'entries' )
186            ) {
187                $this->fatalError( "'users', 'lists' and 'entries' are required in non-cleanup mode" );
188            }
189        }
190    }
191
192}
193
194$maintClass = PopulateWithTestData::class;
195require_once RUN_MAINTENANCE_IF_MAIN;