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