3 namespace MediaWiki\Tests\Maintenance\Includes
;
5 use GenerateSchemaChangeSql
;
7 use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase
;
8 use RecursiveDirectoryIterator
;
9 use RecursiveIteratorIterator
;
12 * @covers \MediaWiki\Maintenance\SchemaMaintenance
13 * @covers \GenerateSchemaChangeSql
14 * @covers \GenerateSchemaSql
16 class SchemaMaintenanceTest
extends MaintenanceBaseTestCase
{
18 private const DATA_DIR
= __DIR__
. '/../../data/schema-maintenance';
20 protected function getMaintenanceClass() {
21 return GenerateSchemaSql
::class;
24 /** @dataProvider provideExecuteForFatalError */
25 public function testExecuteForFatalError( $options, $expectedOutputRegex ) {
26 foreach ( $options as $name => $value ) {
27 $this->maintenance
->setOption( $name, $value );
29 $this->expectCallToFatalError();
30 $this->expectOutputRegex( $expectedOutputRegex );
31 $this->maintenance
->execute();
34 public static function provideExecuteForFatalError() {
36 'Unsupported SQL platform' => [
37 [ 'type' => 'unknown-platform' ], "/'unknown-platform' is not a supported platform/",
42 private function getFileWithContent( string $content ): string {
43 $testFilename = $this->getNewTempFile();
44 $testFile = fopen( $testFilename, 'w' );
45 fwrite( $testFile, $content );
50 /** @dataProvider provideExecuteForFatalErrorWithJsonFileSpecified */
51 public function testExecuteForFatalErrorWithJsonFileSpecified( $options, $fileContent, $expectedOutputRegex ) {
52 $this->testExecuteForFatalError(
53 array_merge( [ 'json' => $this->getFileWithContent( $fileContent ) ], $options ),
58 public static function provideExecuteForFatalErrorWithJsonFileSpecified() {
60 'Validate mode when JSON file empty' => [ [ 'validate' => 1 ], '', '/does not exist/' ],
61 'Validate mode when JSON file is not valid JSON' => [ [ 'validate' => 1 ], '{{{{', '/Invalid JSON/' ],
62 'Validate mode when JSON file is not a valid schema' => [
63 [ 'validate' => 1 ], '{"abc": "test"}', '/did not pass validation/',
65 'JSON file empty when not validating' => [ [], '', '/does not exist/' ],
69 public function testExecuteForSchemaChangeWhenNoSchemaChangesMade() {
70 $maintenance = new GenerateSchemaChangeSql();
71 $maintenance->setOption( 'json', self
::DATA_DIR
. '/patch-no_change.json' );
72 $this->expectCallToFatalError();
73 $this->expectOutputRegex( '/No schema changes detected/' );
74 $maintenance->execute();
77 public function testExecuteForSuccessfulValidationOfJsonFile() {
78 $this->maintenance
->setOption( 'validate', 1 );
79 $this->maintenance
->setOption( 'json', self
::DATA_DIR
. '/tables.json' );
80 $this->maintenance
->execute();
81 $this->expectOutputString( "Schema is valid.\n" );
84 public function testExecuteForSuccessfulValidationOfJsonSchemaChangeFile() {
85 $maintenance = new GenerateSchemaChangeSql();
86 $maintenance->setOption( 'validate', 1 );
87 $maintenance->setOption( 'json', self
::DATA_DIR
. '/patch-drop-ct_tag.json' );
88 $maintenance->execute();
89 $this->expectOutputString( "Schema is valid.\n" );
92 private function assertDirectoryContainsExpectedFiles(
93 string $directoryPath, array $expectedFilePathsToFileContentPath
95 $directory = new RecursiveDirectoryIterator( $directoryPath );
96 $directoryIterator = new RecursiveIteratorIterator( $directory );
98 foreach ( $directoryIterator as $path ) {
99 if ( $path->isDir() ||
$path === $directoryPath ) {
103 // Check that the filename is expected to be present.
104 $this->assertTrue( str_starts_with( $path, $directoryPath ) );
105 $relativePath = substr( $path, strlen( $directoryPath ) );
106 // The expected file may be an array key or an array value. First check for the key, and if not
107 // present then check for array values as long as they key for the value is an integer.
108 if ( array_key_exists( $relativePath, $expectedFilePathsToFileContentPath ) ) {
109 $this->assertArrayHasKey( $relativePath, $expectedFilePathsToFileContentPath );
110 $expectedFileContentPath = $expectedFilePathsToFileContentPath[$relativePath];
112 $keyForPath = array_search( $relativePath, $expectedFilePathsToFileContentPath );
113 $this->assertIsInt( $keyForPath, "$relativePath was not expected" );
114 $expectedFileContentPath = $relativePath;
117 // Fetch the expected content from the phpunit/data/schema-maintenance/ folder and check that the
118 // file here equals that content.
119 $expectedContent = file_get_contents( self
::DATA_DIR
. $expectedFileContentPath );
120 $actualContent = file_get_contents( $path );
121 // Normalise the content such that both the expected and actual content use LF instead of CRLF / VR
122 $expectedContent = str_replace( [ "\r\n", "\r" ], "\n", $expectedContent );
123 $actualContent = str_replace( [ "\r\n", "\r" ], "\n", $actualContent );
128 "The SchemaMaintenance script did not produce the expected SQL."
133 /** @dataProvider provideExecuteForSuccessfulGenerationOfSchemaSql */
134 public function testExecuteForSuccessfulGenerationOfSchemaSql(
135 $options, $shouldMysqlFolderExist, $sqlFilename, $expectedSqlFiles
137 // Get a temporary directory to put the generated SQL files
138 $sqlPath = $this->getNewTempDirectory();
139 if ( $shouldMysqlFolderExist ) {
140 mkdir( $sqlPath . '/mysql' );
142 $sqlPathArgument = $sqlPath;
143 if ( $sqlFilename ) {
144 $sqlPathArgument .= '/' . $sqlFilename;
146 // Run the maintenance script
147 $this->maintenance
->setOption( 'json', realpath( self
::DATA_DIR
) );
148 $this->maintenance
->setOption( 'sql', $sqlPathArgument );
149 foreach ( $options as $name => $value ) {
150 $this->maintenance
->setOption( $name, $value );
152 $this->maintenance
->execute();
153 $this->assertDirectoryContainsExpectedFiles( $sqlPath, $expectedSqlFiles );
154 // Check that the output of the script is as expected
155 $expectedOutputString = '';
156 foreach ( $expectedSqlFiles as $key => $value ) {
157 // The expected filepath is the value, if the key is an integer. Otherwise it is the key.
158 $file = is_int( $key ) ?
$value : $key;
159 $expectedOutputString .= 'Schema change generated and written to ' . $sqlPath . $file . "\n";
161 $this->expectOutputString( $expectedOutputString );
164 public static function provideExecuteForSuccessfulGenerationOfSchemaSql() {
166 'Only mysql' => [ [ 'type' => 'mysql' ], true, '', [ '/mysql/tables-generated.sql' ] ],
167 'Only mysql when mysql folder does not exist' => [
168 [ 'type' => 'mysql' ], false, '', [ '/tables-generated.sql' => '/mysql/tables-generated.sql' ],
170 'Only postgres' => [ [ 'type' => 'postgres' ], true, '', [ '/postgres/tables-generated.sql' ] ],
171 'Only SQLite' => [ [ 'type' => 'sqlite' ], true, '', [ '/sqlite/tables-generated.sql' ] ],
173 [ 'type' => 'all' ], true, '',
174 [ '/mysql/tables-generated.sql', '/sqlite/tables-generated.sql', '/postgres/tables-generated.sql' ],
176 'All types when SQL option is a file' => [
177 [ 'type' => 'all' ], true, 'tables-generated-actor.sql',
179 '/mysql/tables-generated-actor.sql' => '/mysql/tables-generated.sql',
180 '/sqlite/tables-generated-actor.sql' => '/sqlite/tables-generated.sql',
181 '/postgres/tables-generated-actor.sql' => '/postgres/tables-generated.sql'
187 /** @dataProvider provideExecuteForSuccessfulGenerationOfSchemaChangeSql */
188 public function testExecuteForSuccessfulGenerationOfSchemaChangeSql( $options, $expectedSqlFiles ) {
189 // Get a temporary directory to put the generated SQL files
190 $sqlPath = $this->getNewTempDirectory();
191 mkdir( $sqlPath . '/mysql' );
192 // Run the maintenance script
193 $maintenance = new GenerateSchemaChangeSql();
194 $maintenance->setOption( 'json', realpath( self
::DATA_DIR
. '/patch-drop-ct_tag.json' ) );
195 $maintenance->setOption( 'sql', $sqlPath );
196 foreach ( $options as $name => $value ) {
197 $maintenance->setOption( $name, $value );
199 $maintenance->execute();
200 $this->assertDirectoryContainsExpectedFiles( $sqlPath, $expectedSqlFiles );
201 // Check that the output of the script is as expected
202 $expectedOutputString = '';
203 foreach ( $expectedSqlFiles as $file ) {
204 $expectedOutputString .= 'Schema change generated and written to ' . $sqlPath . $file . "\n";
206 $this->expectOutputString( $expectedOutputString );
209 public static function provideExecuteForSuccessfulGenerationOfSchemaChangeSql() {
211 'Only mysql' => [ [ 'type' => 'mysql' ], [ '/mysql/patch-drop-ct_tag.sql' ] ],
212 'Only postgres' => [ [ 'type' => 'postgres' ], [ '/postgres/patch-drop-ct_tag.sql' ] ],
213 'Only SQLite' => [ [ 'type' => 'sqlite' ], [ '/sqlite/patch-drop-ct_tag.sql' ] ],
216 [ '/mysql/patch-drop-ct_tag.sql', '/sqlite/patch-drop-ct_tag.sql', '/postgres/patch-drop-ct_tag.sql' ],
221 public function testExecuteWhenSchemaSqlUnchanged() {
222 // Get a temporary directory and add the mysql tables-generated.sql file into it from the data directory.
223 $sqlPath = $this->getNewTempDirectory();
224 $testFile = fopen( $sqlPath . '/tables-generated.sql', 'w' );
225 fwrite( $testFile, file_get_contents( self
::DATA_DIR
. '/mysql/tables-generated.sql' ) );
227 // Call the maintenance script to generate just the mysql SQL file and check that it outputs no changes were
229 $this->maintenance
->setOption( 'json', realpath( self
::DATA_DIR
) );
230 $this->maintenance
->setOption( 'sql', $sqlPath . '/tables-generated.sql' );
231 $this->maintenance
->execute();
232 $this->expectOutputString(
233 "Schema change is unchanged.\n" .
234 'Schema change generated and written to ' . $sqlPath . "/tables-generated.sql\n"
236 // Check that the test file has not been changed
237 $contentBeforeCall = file_get_contents( self
::DATA_DIR
. '/mysql/tables-generated.sql' );
238 $contentAfterCall = file_get_contents( $sqlPath . '/tables-generated.sql' );
239 $this->assertSame( $contentBeforeCall, $contentAfterCall );