├── .github └── workflows │ ├── php.yml │ └── phpstan.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml.dist ├── run_tests.sh ├── src ├── ConfigReader.php ├── Migrator.php ├── SchemaCleaner.php └── TestConnectionManager.php └── tests ├── .env.Mysql ├── .env.Postgres ├── .env.Sqlite ├── Plugins ├── BarPlugin │ └── config │ │ └── Migrations │ │ └── 20200208100000_bar_migration.php └── FooPlugin │ └── config │ └── Migrations │ └── 20200208100000_foo_migration.php ├── TestApp ├── config │ └── Migrations │ │ ├── 20200208100000_add_articles.php │ │ ├── 20200209100000_populate_articles.php │ │ └── 20200210100000_create_view.php └── src │ └── Model │ └── Table │ └── ArticlesTable.php ├── TestCase ├── ConfigReaderTest.php ├── MigratorTest.php ├── SchemaCleanerDumpTest.php ├── SchemaCleanerListTablesTest.php ├── SchemaCleanerTest.php └── TestConnectionManagerTest.php └── bootstrap.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: GitHub CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | schedule: 11 | - cron: '0 0 * * 0' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | php-version: ['8.1', '8.2'] 20 | db-type: [sqlite, mysql, pgsql] 21 | composer-type: [lowest, stable, dev] 22 | exclude: 23 | - php-version: '8.1' 24 | db-type: sqlite 25 | composer-type: lowest 26 | 27 | name: PHP ${{ matrix.php-version }} & ${{ matrix.db-type }} & ${{ matrix.composer-type }} 28 | 29 | services: 30 | postgres: 31 | image: postgres 32 | ports: 33 | - 5432:5432 34 | env: 35 | POSTGRES_DB: test_migrator 36 | POSTGRES_PASSWORD: root 37 | POSTGRES_USER: root 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - name: Setup PHP 43 | uses: shivammathur/setup-php@v2 44 | with: 45 | php-version: ${{ matrix.php-version }} 46 | extensions: mbstring, intl, apcu, pdo_${{ matrix.db-type }} 47 | ini-values: apc.enable_cli = 1 48 | 49 | - name: Update composer 50 | run: composer self-update 51 | 52 | - name: Validate composer.json 53 | run: composer validate 54 | 55 | - name: Install dependencies 56 | run: | 57 | if [[ ${{ matrix.composer-type }} == 'lowest' ]]; then 58 | composer update --prefer-dist --no-progress --no-suggest --prefer-stable --prefer-lowest 59 | elif [[ ${{ matrix.composer-type }} == 'stable' ]]; then 60 | composer update --prefer-dist --no-progress --no-suggest --prefer-stable 61 | else 62 | composer update --prefer-dist --no-progress --no-suggest 63 | fi 64 | 65 | - name: Run tests 66 | run: | 67 | if [ ${{ matrix.db-type }} == 'mysql' ]; then 68 | sudo service mysql start && mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE IF NOT EXISTS test_migrator;'; 69 | fi 70 | composer ${{ matrix.db-type }} 71 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | php-version: ['8.1'] 19 | 20 | name: PHP ${{ matrix.php-version }} 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php-version }} 29 | extensions: mbstring, intl, apcu 30 | ini-values: apc.enable_cli = 1 31 | 32 | - name: Update composer 33 | run: composer self-update 34 | 35 | - name: Validate composer.json 36 | run: composer validate 37 | 38 | - name: Install composer 39 | run: composer update --prefer-dist --no-progress --no-suggest --prefer-stable 40 | 41 | - name: Install PHPStan 42 | run: composer stan-setup 43 | 44 | - name: Run phpstan 45 | run: composer phpstan 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /composer.lock 3 | /vendor/ 4 | /.phpunit.cache 5 | **/test_migrator 6 | tests/.env 7 | /tmp 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 vierge-noire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cakephp-test-migrator 2 | A tool to run migrations prior to running tests 3 | 4 | 5 | ## The Migrator 6 | 7 | #### For CakePHP 3.x 8 | composer require --dev vierge-noire/cakephp-test-migrator "^1.0" 9 | 10 | #### For CakePHP 4.x 11 | composer require --dev vierge-noire/cakephp-test-migrator "^2.0" 12 | 13 | #### For CakePHP 4.3+ 14 | The migrator was integrated into the migration plugin (see the [doc](https://book.cakephp.org/migrations/3/en/index.html#using-migrations-for-tests)). The present package will not be further maintained. 15 | 16 | ### Introduction 17 | 18 | CakePHP fixtures handle the test DB schema in a paralell manner to the default DB. On the one hand you will write migrations for your default DB. On the other hand you either hard coded describe the schema structure in your fixtures, or meme the default DB. The later is simpler, but it forces you to have two DBs. And in CI tools, you will have to run the migrations on your default DB, and the fixtures meme the default DB. So why not running migrations directly on the test DB? 19 | 20 | With the CakePHP Test Migrator, the schema of both default and test DB are handled exactly in the same way. You do not necessarily need a default DB. Tables are not dropped between test suites, which speeds up your tests. And migrations are part of the whole testing process: they get indirectly tested. 21 | 22 | ### Setting 23 | 24 | The package proposes a tool to run your [migrations](https://book.cakephp.org/migrations/3/en/index.html) once prior to the tests. In order to do so, 25 | you may place the following in your `tests/bootstrap.php`: 26 | ```php 27 | \CakephpTestMigrator\Migrator::migrate(); 28 | ``` 29 | This command will ensure that your migrations are well run and keeps the test DB(s) up to date. Since tables are truncated but never dropped by the present package's fixture manager, migrations will be run strictly when needed, namely only after a new migration was created by the developer. 30 | 31 | The `Migrator`approach presents the following advantages: 32 | * it improves the speed of the test suites by avoiding the creation and dropping of tables between each test case classes, 33 | * it eases the maintenance of your tests, since regular and test DBs are managed the same way, 34 | * it indirectly tests your migrations. 35 | 36 | You may pass `true` as the second argument for a verbose output on the console. 37 | 38 | ### Options 39 | 40 | | name | type | default | description | 41 | | ---- | ---- | ------- | ----------- | 42 | | verbose | *bool* | `false` | Print info about migrations done | 43 | | truncate | *bool* | `true` | Truncate all tables after migrations are done. You can call `truncate()` manually | 44 | 45 | You can pass a boolean as options, it will be used as `verbose` 46 | 47 | ```php 48 | \CakephpTestMigrator\Migrator::migrate([], true); 49 | // is the same as 50 | \CakephpTestMigrator\Migrator::migrate([], ['verbose' => true]); 51 | ``` 52 | 53 | ### Multiple migrations settings 54 | 55 | You can pass the various migrations directly in the Migrator instantiation: 56 | ```php 57 | \CakephpTestMigrator\Migrator::migrate([ 58 | ['connection' => 'test', 'source' => 'TestFolder'], 59 | ['plugin' => 'FooPlugin', 'connection' => 'FooConnection'], 60 | ['source' => 'BarFolder'], 61 | ... 62 | ], ['verbose' => true]); 63 | ``` 64 | 65 | You can also pass the various migrations directly in your Datasource configuration, under the key `migrations`: 66 | ```php 67 | // In config/app.php 68 | 'test' => [ 69 | 'className' => Connection::class, 70 | 'driver' => Mysql::class, 71 | 'persistent' => false, 72 | 'timezone' => 'UTC', 73 | 'flags' => [], 74 | 'cacheMetadata' => true, 75 | 'quoteIdentifiers' => false, 76 | 'log' => false, 77 | 'migrations' => [ 78 | ['plugin' => 'FooPlugin'], 79 | ['source' => 'BarFolder'], 80 | ], 81 | ], 82 | ``` 83 | 84 | You can set `migrations` simply to `true` if you which to use the default migration settings. 85 | 86 | ### Migrations status 87 | 88 | Information on a connection's migration status will be obtained as follows: 89 | ```php 90 | $migrator = Migrator::migrate(); 91 | $connectionsWithModifiedStatus = $migrator->getConnectionsWithModifiedStatus(); 92 | ``` 93 | 94 | the method `getConnectionsWithModifiedStatus` returning a list of the connections with down 95 | migrations prior to running the migrations. 96 | 97 | ### Data truncation 98 | 99 | Use **truncate** option to truncate (or not) after migrations are done. This is useful if you need to do other tasks in the test suite bootstrap 100 | 101 | ```php 102 | $migrator = Migrator::migrate([], ['truncate' => false]); 103 | // do something with the migrated database (bake fixtures, etc) 104 | $migrator->truncate(); 105 | ``` 106 | 107 | ### What happens if I switch branches? 108 | 109 | If you ever switched to a branch with nonexistent up migrations, you've moved to a branch in a past state. 110 | The `Migrator` will automatically drop the tables where needed, and re-run the migrations. Switching branches therefore 111 | does not require any intervention on your side. 112 | 113 | ### What if I do not use migrations? 114 | 115 | The `Migrator::dump()` will help you import any schema from one or several sql file. Run for example: 116 | 117 | ```php 118 | Migrator::dump('test', 'path/to/file.sql') 119 | ``` 120 | or with multiples files 121 | ```php 122 | Migrator::dump('test', [ 123 | 'path1/to/file1.sql', 124 | 'path2/to/file2.sql', 125 | ]) 126 | ``` 127 | or for a verbose output 128 | ```php 129 | Migrator::dump('test', 'path/to/file.sql', true) 130 | ``` 131 | 132 | The first argument is the name of the connection, the second the file(s) to dump, the third the verbosity (boolean). 133 | 134 | This method will however drop the schema prior to recreating it, which presents a significant loss of 135 | performance in comparison to the migration-based solution. 136 | 137 | ### Trouble shooting 138 | 139 | It might be required, right after you installed or updated the plugin, to drop and recreate your test database. If the problem persists, feel free to open an issue. 140 | 141 | ## Authors 142 | * Juan Pablo Ramirez 143 | * Nicolas Masson 144 | 145 | 146 | ## Support 147 | Contact us at vierge.noire.info@gmail.com for professional assistance. 148 | 149 | You like our work? [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L3L52P9JA) 150 | 151 | 152 | ## License 153 | 154 | The CakephpTestMigrator plugin is offered under an [MIT license](https://opensource.org/licenses/mit-license.php). 155 | 156 | Copyright 2020 Juan Pablo Ramirez and Nicolas Masson 157 | 158 | Licensed under The MIT License Redistributions of files must retain the above copyright notice. 159 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vierge-noire/cakephp-test-migrator", 3 | "description": "Migration helper for the CakePHP Test Suite Light", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Juan Pablo Ramirez", 8 | "email": "pabloelcolombiano@gmail.com" 9 | } 10 | ], 11 | "minimum-stability": "dev", 12 | "prefer-stable": true, 13 | "require": { 14 | "cakephp/cakephp": "5.x-dev" 15 | }, 16 | "require-dev": { 17 | "cakephp/cakephp-codesniffer": "^5.1", 18 | "cakephp/migrations": "4.x-dev as 4.0.0", 19 | "josegonzalez/dotenv": "dev-master", 20 | "phpunit/phpunit": "^10.1" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "CakephpTestMigrator\\": "src" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "CakephpTestMigrator\\Test\\": "tests", 30 | "MigratorTestApp\\": "tests\\TestApp\\src" 31 | } 32 | }, 33 | "scripts": { 34 | "cs-check": "phpcs --colors --parallel=16 -p src/ tests/", 35 | "cs-fix": "phpcbf --colors --parallel=16 -p src/ tests/", 36 | "mysql": "bash run_tests.sh Mysql", 37 | "pgsql": "bash run_tests.sh Postgres", 38 | "sqlite": "bash run_tests.sh Sqlite", 39 | "phpstan": "./vendor/bin/phpstan analyse --memory-limit=-1", 40 | "phpstan-baseline": "./vendor/bin/phpstan --generate-baseline", 41 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:1.10.18 && mv composer.backup composer.json" 42 | }, 43 | "config": { 44 | "sort-packages": true, 45 | "allow-plugins": { 46 | "dealerdirect/phpcodesniffer-composer-installer": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Call to an undefined method Cake\\\\Datasource\\\\ConnectionInterface\\:\\:execute\\(\\)\\.$#" 5 | count: 1 6 | path: src/Migrator.php 7 | 8 | - 9 | message: "#^Call to an undefined method Cake\\\\Datasource\\\\ConnectionInterface\\:\\:disableConstraints\\(\\)\\.$#" 10 | count: 1 11 | path: src/SchemaCleaner.php 12 | 13 | - 14 | message: "#^Call to an undefined method Cake\\\\Datasource\\\\ConnectionInterface\\:\\:execute\\(\\)\\.$#" 15 | count: 2 16 | path: src/SchemaCleaner.php 17 | 18 | - 19 | message: "#^Call to an undefined method Cake\\\\Datasource\\\\ConnectionInterface\\:\\:getSchemaCollection\\(\\)\\.$#" 20 | count: 2 21 | path: src/SchemaCleaner.php 22 | 23 | - 24 | message: "#^Call to an undefined method Cake\\\\Datasource\\\\ConnectionInterface\\:\\:transactional\\(\\)\\.$#" 25 | count: 1 26 | path: src/SchemaCleaner.php 27 | 28 | - 29 | message: "#^Call to an undefined method object\\:\\:schemaDialect\\(\\)\\.$#" 30 | count: 1 31 | path: src/SchemaCleaner.php 32 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | checkMissingIterableValueType: false 7 | bootstrapFiles: 8 | - tests/bootstrap.php 9 | paths: 10 | - src/ -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ./tests/TestCase/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | ./src/ 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DRIVER=$1; 4 | 5 | echo "Starting PHPUNIT tests" 6 | export DB_DRIVER=$DRIVER 7 | 8 | ./vendor/bin/phpunit --testsuite Default --stop-on-fail 9 | -------------------------------------------------------------------------------- /src/ConfigReader.php: -------------------------------------------------------------------------------- 1 | getActiveConnections() as $connectionName) { 34 | $connection = ConnectionManager::getConfig($connectionName); 35 | $config = []; 36 | 37 | if (is_array($connection)) { 38 | $migrations = $connection['migrations'] ?? false; 39 | 40 | if ($migrations) { 41 | if ($migrations === true) { 42 | $config = ['connection' => $connectionName ]; 43 | $this->normalizeArray($config); 44 | } elseif (is_array($migrations)) { 45 | $config = $migrations; 46 | $this->normalizeArray($config); 47 | foreach ($config as $k => $v) { 48 | $config[$k]['connection'] = $v['connection'] ?? $connectionName; 49 | } 50 | } 51 | $this->config = array_merge($this->config, $config); 52 | } 53 | } 54 | } 55 | 56 | $this->processConfig(); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @param array|array $config An array of migration configs 63 | * @return $this 64 | */ 65 | public function readConfig(array $config = []) 66 | { 67 | if (!empty($config)) { 68 | $this->normalizeArray($config); 69 | $this->config = $config; 70 | } 71 | 72 | $this->processConfig(); 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Make sure config is set properly 79 | * 80 | * @return void 81 | */ 82 | public function processConfig(): void 83 | { 84 | foreach ($this->config as $k => $config) { 85 | $this->config[$k]['connection'] = $this->config[$k]['connection'] ?? 'test'; 86 | } 87 | if (empty($this->config)) { 88 | $this->config = [['connection' => 'test']]; 89 | } 90 | } 91 | 92 | /** 93 | * Initialize all connections used by the manager 94 | * 95 | * @return array 96 | */ 97 | public function getActiveConnections(): array 98 | { 99 | $connections = ConnectionManager::configured(); 100 | foreach ($connections as $i => $connectionName) { 101 | if ($this->skipConnection($connectionName)) { 102 | unset($connections[$i]); 103 | } 104 | } 105 | 106 | return $connections; 107 | } 108 | 109 | /** 110 | * @param string $connectionName Connection name 111 | * @return bool 112 | */ 113 | public function skipConnection(string $connectionName): bool 114 | { 115 | // CakePHP 4 solves a DebugKit issue by creating an Sqlite connection 116 | // in tests/bootstrap.php. This connection should be ignored. 117 | if ($connectionName === 'test_debug_kit') { 118 | return true; 119 | } 120 | 121 | if ($connectionName === 'test' || strpos($connectionName, 'test_') === 0) { 122 | return false; 123 | } 124 | 125 | return true; 126 | } 127 | 128 | /** 129 | * Make array an array of arrays 130 | * 131 | * @param array $array 132 | * @return void 133 | */ 134 | public function normalizeArray(array &$array): void 135 | { 136 | if (!empty($array) && !isset($array[0])) { 137 | $array = [$array]; 138 | } 139 | } 140 | 141 | /** 142 | * @return array 143 | */ 144 | public function getConfig(): array 145 | { 146 | return $this->config; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Migrator.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $connectionsWithModifiedStatus = []; 37 | 38 | /** 39 | * Migrator constructor. 40 | * 41 | * @param bool $verbose 42 | * @param null $configReader 43 | */ 44 | final public function __construct(bool $verbose, ?ConfigReader $configReader = null) 45 | { 46 | $this->io = new ConsoleIo(); 47 | $this->io->level($verbose ? ConsoleIo::NORMAL : ConsoleIo::QUIET); 48 | $this->configReader = $configReader ?? new ConfigReader(); 49 | 50 | // Make sure that the connections are aliased, in case 51 | // the migrations invoke the table registry. 52 | TestConnectionManager::aliasConnections(); 53 | } 54 | 55 | /** 56 | * General command to run before your tests run 57 | * E.g. in tests/bootstrap.php 58 | * 59 | * Options: 60 | * - verbose | bool | Set to true to display messages 61 | * - truncate | bool | Truncate tables after migrations are done. 62 | * 63 | * @param array $config 64 | * @param array{verbose?:bool,truncate?:bool}|bool $options Options for migrations 65 | * @return self 66 | */ 67 | public static function migrate(array $config = [], array|bool $options = []): Migrator 68 | { 69 | if ($options === true || $options === false) { 70 | $options = ['verbose' => $options]; 71 | } 72 | $options += [ 73 | 'verbose' => false, 74 | 'truncate' => true, 75 | ]; 76 | $migrator = new static($options['verbose']); 77 | 78 | $migrator->configReader->readMigrationsInDatasources(); 79 | $migrator->configReader->readConfig($config); 80 | $migrator->handleMigrationsStatus(); 81 | 82 | if ($options['truncate']) { 83 | $migrator->truncate(); 84 | } 85 | 86 | return $migrator; 87 | } 88 | 89 | /** 90 | * Import the schema from a file, or an array of files. 91 | * 92 | * @param string $connectionName Connection 93 | * @param array|string $file File to dump 94 | * @param bool $verbose Set to true to display messages 95 | * @return void 96 | * @throws \Exception if the truncation failed 97 | * @throws \RuntimeException if the file could not be processed 98 | */ 99 | public static function dump(string $connectionName, string|array $file, bool $verbose = false): void 100 | { 101 | $files = (array)$file; 102 | 103 | $migrator = new static($verbose); 104 | $schemaCleaner = new SchemaCleaner($migrator->io); 105 | $schemaCleaner->drop($connectionName); 106 | 107 | foreach ($files as $file) { 108 | if (!file_exists($file)) { 109 | throw new RuntimeException('The file ' . $file . ' could not found.'); 110 | } 111 | 112 | $sql = file_get_contents($file); 113 | if ($sql === false) { 114 | throw new RuntimeException('The file ' . $file . ' could not read.'); 115 | } 116 | 117 | ConnectionManager::get($connectionName)->execute($sql); 118 | 119 | $migrator->io->success( 120 | 'Dump of schema in file ' . $file . ' for connection ' . $connectionName . ' successful.' 121 | ); 122 | } 123 | 124 | $schemaCleaner->truncate($connectionName); 125 | } 126 | 127 | /** 128 | * Run migrations for all configured migrations. 129 | * 130 | * @param array $config Migration configuration. 131 | * @return void 132 | */ 133 | protected function runMigrations(array $config): void 134 | { 135 | $migrations = new Migrations(); 136 | $result = $migrations->migrate($config); 137 | 138 | $msg = 'Migrations for ' . $this->stringifyConfig($config); 139 | 140 | if ($result === true) { 141 | $this->io->success($msg . ' successfully run.'); 142 | } else { 143 | $this->io->error($msg . ' failed.'); 144 | } 145 | } 146 | 147 | /** 148 | * Truncates connections on demand. 149 | * 150 | * @param array|null $connections Connections names to truncate. Defaults to modified connections 151 | * @return void 152 | */ 153 | public function truncate(?array $connections = null): void 154 | { 155 | if ($connections === null) { 156 | $connections = $this->configReader->getActiveConnections(); 157 | } 158 | $schemaCleaner = new SchemaCleaner($this->io); 159 | foreach ($connections as $connectionName) { 160 | $schemaCleaner->truncate($connectionName); 161 | } 162 | } 163 | 164 | /** 165 | * If a migration is missing or down, all tables of the considered connection are dropped. 166 | * 167 | * @return $this 168 | */ 169 | protected function handleMigrationsStatus() 170 | { 171 | $schemaCleaner = new SchemaCleaner($this->io); 172 | foreach ($this->getConfigs() as &$config) { 173 | $connectionName = $config['connection'] = $config['connection'] ?? 'test'; 174 | $this->io->info("Reading migrations status for {$this->stringifyConfig($config)}..."); 175 | $migrations = new Migrations($config); 176 | if ($this->isStatusChanged($migrations)) { 177 | if (!in_array($connectionName, $this->connectionsWithModifiedStatus)) { 178 | $this->connectionsWithModifiedStatus[] = $connectionName; 179 | } 180 | } 181 | } 182 | 183 | if (empty($this->connectionsWithModifiedStatus)) { 184 | $this->io->success('No migration changes detected.'); 185 | 186 | return $this; 187 | } 188 | 189 | foreach ($this->connectionsWithModifiedStatus as $connectionName) { 190 | $schemaCleaner->drop($connectionName); 191 | } 192 | 193 | foreach ($this->getConfigs() as $config) { 194 | $this->runMigrations($config); 195 | } 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Checks if any migrations are up but missing. 202 | * 203 | * @param \Migrations\Migrations $migrations 204 | * @return bool 205 | */ 206 | protected function isStatusChanged(Migrations $migrations): bool 207 | { 208 | foreach ($migrations->status() as $migration) { 209 | if ($migration['status'] === 'up' && ($migration['missing'] ?? false)) { 210 | $this->io->info('Missing migration(s) detected.'); 211 | 212 | return true; 213 | } 214 | if ($migration['status'] === 'down') { 215 | $this->io->info('New migration(s) found.'); 216 | 217 | return true; 218 | } 219 | } 220 | 221 | return false; 222 | } 223 | 224 | /** 225 | * Stringify the migration parameters. 226 | * 227 | * @param array $config Config array 228 | * @return string 229 | */ 230 | protected function stringifyConfig(array $config): string 231 | { 232 | $options = []; 233 | foreach (['connection', 'plugin', 'source', 'target'] as $option) { 234 | if (isset($config[$option])) { 235 | $options[] = $option . ' "' . $config[$option] . '"'; 236 | } 237 | } 238 | 239 | return implode(', ', $options); 240 | } 241 | 242 | /** 243 | * @return array 244 | */ 245 | public function getConfigs(): array 246 | { 247 | return $this->getConfigReader()->getConfig(); 248 | } 249 | 250 | /** 251 | * @return \CakephpTestMigrator\ConfigReader 252 | */ 253 | protected function getConfigReader(): ConfigReader 254 | { 255 | return $this->configReader; 256 | } 257 | 258 | /** 259 | * Returns an array of strings with all the connections 260 | * which migration status have changed and were migrated. 261 | * 262 | * @return array 263 | */ 264 | public function getConnectionsWithModifiedStatus(): array 265 | { 266 | return $this->connectionsWithModifiedStatus; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/SchemaCleaner.php: -------------------------------------------------------------------------------- 1 | io = $io; 40 | } 41 | 42 | /** 43 | * Drop all tables of the provided connection. 44 | * 45 | * @param string $connectionName Name of the connection. 46 | * @return void 47 | * @throws \Exception if the dropping failed. 48 | */ 49 | public function drop(string $connectionName): void 50 | { 51 | $this->info("Dropping all tables for connection {$connectionName}."); 52 | 53 | $connection = ConnectionManager::get($connectionName); 54 | $stmts = []; 55 | foreach ($this->listTables($connection) as $table) { 56 | $table = $connection->getSchemaCollection()->describe($table); 57 | if ($table instanceof TableSchema) { 58 | $driver = $connection->getDriver(); 59 | $stmts = array_merge($stmts, $driver->schemaDialect()->dropTableSql($table)); 60 | } 61 | } 62 | 63 | $this->executeStatements(ConnectionManager::get($connectionName), $stmts); 64 | } 65 | 66 | /** 67 | * Truncate all tables of the provided connection. 68 | * 69 | * @param string $connectionName Name of the connection. 70 | * @return void 71 | * @throws \Exception if the truncation failed. 72 | */ 73 | public function truncate(string $connectionName): void 74 | { 75 | $this->info("Truncating all tables for connection {$connectionName}."); 76 | 77 | /** 78 | * @var \Cake\Database\Connection $connection 79 | */ 80 | $connection = ConnectionManager::get($connectionName); 81 | $stmts = []; 82 | $tables = $this->listTables($connection); 83 | $tables = $this->unsetMigrationTables($tables); 84 | foreach ($tables as $table) { 85 | $table = $connection->getSchemaCollection()->describe($table); 86 | if ($table instanceof TableSchema) { 87 | $driver = $connection->getDriver(); 88 | $stmts = array_merge($stmts, $driver->schemaDialect()->truncateTableSql($table)); 89 | } 90 | } 91 | 92 | $this->executeStatements($connection, $stmts); 93 | } 94 | 95 | /** 96 | * List sall tables, without views. 97 | * 98 | * @param \Cake\Datasource\ConnectionInterface $connection Connection. 99 | * @return array 100 | */ 101 | public function listTables(ConnectionInterface $connection): array 102 | { 103 | $driver = $connection->getDriver(); 104 | if ($driver instanceof Mysql) { 105 | $query = 'SHOW FULL TABLES WHERE Table_Type != "VIEW"'; 106 | } elseif ($driver instanceof Postgres) { 107 | $query = "SELECT tablename AS TABLE FROM pg_tables WHERE schemaname = 'public'"; 108 | } else { 109 | return $connection->getSchemaCollection()->listTables(); 110 | } 111 | 112 | $result = $connection->execute($query)->fetchAll(); 113 | if ($result === false) { 114 | throw new PDOException($query . ' failed'); 115 | } 116 | 117 | return (array)Hash::extract($result, '{n}.0'); 118 | } 119 | 120 | /** 121 | * @param \Cake\Datasource\ConnectionInterface $connection Connection. 122 | * @param array $commands Sql commands to run 123 | * @return void 124 | * @throws \Exception 125 | */ 126 | protected function executeStatements(ConnectionInterface $connection, array $commands): void 127 | { 128 | $connection->disableConstraints( 129 | function (ConnectionInterface $connection) use ($commands): void { 130 | $connection->transactional( 131 | function (ConnectionInterface $connection) use ($commands): void { 132 | foreach ($commands as $sql) { 133 | $connection->execute($sql); 134 | } 135 | } 136 | ); 137 | } 138 | ); 139 | } 140 | 141 | /** 142 | * @param string $msg Message to display. 143 | * @return void 144 | */ 145 | protected function info(string $msg): void 146 | { 147 | if ($this->io instanceof ConsoleIo) { 148 | $this->io->info($msg); 149 | } 150 | } 151 | 152 | /** 153 | * Unset the phinx migration tables from an array of tables. 154 | * 155 | * @param array $tables 156 | * @return array 157 | */ 158 | public function unsetMigrationTables(array $tables): array 159 | { 160 | $endsWithPhinxlog = function (string $string) { 161 | $needle = 'phinxlog'; 162 | 163 | return substr($string, -strlen($needle)) === $needle; 164 | }; 165 | 166 | foreach ($tables as $i => $table) { 167 | if ($endsWithPhinxlog($table)) { 168 | unset($tables[$i]); 169 | } 170 | } 171 | 172 | return array_values($tables); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/TestConnectionManager.php: -------------------------------------------------------------------------------- 1 | _aliasConnections(); 32 | self::$aliasConnectionIsLoaded = true; 33 | } 34 | } 35 | 36 | /** 37 | * Add aliases for all non test prefixed connections. 38 | * 39 | * This allows models to use the test connections without 40 | * a pile of configuration work. 41 | * 42 | * @return void 43 | */ 44 | protected function _aliasConnections(): void 45 | { 46 | $connections = ConnectionManager::configured(); 47 | ConnectionManager::alias('test', 'default'); 48 | $map = []; 49 | foreach ($connections as $connection) { 50 | if ($connection === 'test' || $connection === 'default') { 51 | continue; 52 | } 53 | if (isset($map[$connection])) { 54 | continue; 55 | } 56 | if (strpos($connection, 'test_') === 0) { 57 | $map[$connection] = substr($connection, 5); 58 | } else { 59 | $map['test_' . $connection] = $connection; 60 | } 61 | } 62 | foreach ($map as $testConnection => $normal) { 63 | ConnectionManager::alias($testConnection, $normal); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/.env.Mysql: -------------------------------------------------------------------------------- 1 | DB_DRIVER=Mysql 2 | DB_USER=root 3 | DB_PWD=root 4 | DB_HOST=localhost 5 | DB_DATABASE=test_migrator 6 | -------------------------------------------------------------------------------- /tests/.env.Postgres: -------------------------------------------------------------------------------- 1 | DB_DRIVER=Postgres 2 | DB_USER=root 3 | DB_PWD=root 4 | DB_HOST=localhost 5 | DB_DATABASE=test_migrator 6 | -------------------------------------------------------------------------------- /tests/.env.Sqlite: -------------------------------------------------------------------------------- 1 | DB_DRIVER=Sqlite 2 | DB_USER=root 3 | DB_PWD=root 4 | DB_HOST=localhost 5 | DB_DATABASE=test_migrator 6 | -------------------------------------------------------------------------------- /tests/Plugins/BarPlugin/config/Migrations/20200208100000_bar_migration.php: -------------------------------------------------------------------------------- 1 | table('articles') 22 | ->addPrimaryKey(['id']) 23 | ->addColumn( 24 | 'title', 25 | 'string', 26 | [ 27 | 'limit' => 128, 28 | 'null' => false, 29 | ] 30 | ) 31 | ->addTimestamps('created', 'modified') 32 | ->create(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/TestApp/config/Migrations/20200209100000_populate_articles.php: -------------------------------------------------------------------------------- 1 | table('articles')->insert(['title' => 'foo'])->save(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/TestApp/config/Migrations/20200210100000_create_view.php: -------------------------------------------------------------------------------- 1 | execute( 24 | 'CREATE VIEW ' . SchemaCleanerListTablesTest::VIEW_NAME . " AS SELECT '" . SchemaCleanerListTablesTest::VIEW_CONTENT . "'" 25 | ); 26 | } catch (Throwable $e) { 27 | // Do nothing, the view might already exist. 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/TestApp/src/Model/Table/ArticlesTable.php: -------------------------------------------------------------------------------- 1 | ConfigReader = new ConfigReader(); 29 | } 30 | 31 | public function tearDown(): void 32 | { 33 | unset($this->ConfigReader); 34 | } 35 | 36 | public function testSetConfigFromInjection(): void 37 | { 38 | $config = [ 39 | ['connection' => 'Foo', 'plugin' => 'Bar',], 40 | ['plugin' => 'Bar',], 41 | ]; 42 | 43 | $expect = [ 44 | ['connection' => 'Foo', 'plugin' => 'Bar',], 45 | ['plugin' => 'Bar', 'connection' => 'test',], 46 | ]; 47 | 48 | $this->ConfigReader->readConfig($config); 49 | 50 | $this->assertSame($expect, $this->ConfigReader->getConfig()); 51 | } 52 | 53 | public function testSetConfigFromEmptyInjection(): void 54 | { 55 | $expect = [ 56 | ['connection' => 'test'], 57 | ]; 58 | 59 | $this->ConfigReader->readConfig(); 60 | 61 | $this->assertSame($expect, $this->ConfigReader->getConfig()); 62 | } 63 | 64 | public function testSetConfigWithConfigureAndInjection(): void 65 | { 66 | $config1 = [ 67 | 'connection' => 'Foo1_testSetConfigWithConfigureAndInjection', 68 | 'plugin' => 'Bar1_testSetConfigWithConfigureAndInjection', 69 | ]; 70 | 71 | $this->ConfigReader->readConfig($config1); 72 | $this->assertSame([$config1], $this->ConfigReader->getConfig()); 73 | } 74 | 75 | public function testReadMigrationsInDatasource(): void 76 | { 77 | $this->ConfigReader->readMigrationsInDatasources(); 78 | // Read empty config will not overwrite Datasource config 79 | $this->ConfigReader->readConfig(); 80 | $act = $this->ConfigReader->getConfig(); 81 | $expected = [ 82 | ['source' => 'FooSource', 'connection' => 'test'], 83 | ['plugin' => 'FooPlugin', 'connection' => 'test'], 84 | ['plugin' => 'BarPlugin', 'connection' => 'test_2'], 85 | ['connection' => 'test_3'], 86 | ]; 87 | $this->assertSame($expected, $act); 88 | } 89 | 90 | public function testReadMigrationsInDatasourceAndInjection(): void 91 | { 92 | $this->ConfigReader->readMigrationsInDatasources(); 93 | // Read non-empty config will overwrite Datasource config 94 | $this->ConfigReader->readConfig(['source' => 'Foo']); 95 | $act = $this->ConfigReader->getConfig(); 96 | $expected = [ 97 | ['source' => 'Foo', 'connection' => 'test'], 98 | ]; 99 | $this->assertSame($expected, $act); 100 | } 101 | 102 | public static function arrays(): array 103 | { 104 | return [ 105 | [['a' => 'b'], [['a' => 'b']]], 106 | [[['a' => 'b']], [['a' => 'b']]], 107 | [[], []], 108 | ]; 109 | } 110 | 111 | /** 112 | * @dataProvider arrays 113 | * @param array $input 114 | * @param array $expect 115 | */ 116 | public function testNormalizeArray(array $input, array $expect): void 117 | { 118 | $this->ConfigReader->normalizeArray($input); 119 | $this->assertSame($expect, $input); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/TestCase/MigratorTest.php: -------------------------------------------------------------------------------- 1 | selectQuery() 29 | ->select('migration_name') 30 | ->from($dbTable) 31 | ->execute() 32 | ->fetchAll(); 33 | 34 | return (array)Hash::extract($result, '{n}.0'); 35 | } 36 | 37 | public function testGetConfigFromDatasource(): void 38 | { 39 | $expect = [ 40 | ['source' => 'FooSource', 'connection' => 'test'], 41 | ['plugin' => 'FooPlugin', 'connection' => 'test'], 42 | ['plugin' => 'BarPlugin', 'connection' => 'test_2'], 43 | ['connection' => 'test_3'], 44 | ]; 45 | $migrator = Migrator::migrate(); 46 | $config = $migrator->getConfigs(); 47 | $this->assertSame($expect, $config); 48 | } 49 | 50 | public function testMigrate(): void 51 | { 52 | Migrator::migrate(); 53 | 54 | $appMigrations = $this->fetchMigrationsInDB('phinxlog'); 55 | $fooPluginMigrations = $this->fetchMigrationsInDB('foo_plugin_phinxlog'); 56 | $barPluginMigrations = $this->fetchMigrationsInDB('bar_plugin_phinxlog'); 57 | 58 | $this->assertSame(['AddArticles', 'PopulateArticles', 'CreateView'], $appMigrations); 59 | $this->assertSame(['FooMigration'], $fooPluginMigrations); 60 | $this->assertSame(['BarMigration'], $barPluginMigrations); 61 | } 62 | 63 | public function testGetConnectionsWithModifiedStatus(): void 64 | { 65 | $connections = [ 66 | 'test', 67 | 'test_2', 68 | 'test_3', 69 | ]; 70 | 71 | // Run the migrations once to make sure that all are up to date. 72 | Migrator::migrate(); 73 | 74 | // Status did not changed. 75 | $migrator = Migrator::migrate(); 76 | $this->assertSame([], $migrator->getConnectionsWithModifiedStatus()); 77 | 78 | // Drop all connections' tables. Statuses are reset. 79 | $cleaner = new SchemaCleaner(); 80 | foreach ($connections as $connection) { 81 | $cleaner->drop($connection); 82 | } 83 | 84 | // All connections were touched by the migrations. 85 | $migrator = Migrator::migrate(); 86 | $connectionsWithModifiedStatus = $migrator->getConnectionsWithModifiedStatus(); 87 | $this->assertSame($connections, $connectionsWithModifiedStatus); 88 | } 89 | 90 | public function testTableRegistryConnectionName(): void 91 | { 92 | $Articles = TableRegistry::getTableLocator()->get('Articles'); 93 | ConnectionManager::getConfigOrFail('default'); 94 | $this->assertSame('test', $Articles->getConnection()->configName()); 95 | } 96 | 97 | public function testDropTablesForMissingMigrations(): void 98 | { 99 | $connection = ConnectionManager::get('test'); 100 | $connection->insert('phinxlog', ['version' => 1, 'migration_name' => 'foo',]); 101 | 102 | $count = $connection->selectQuery()->select('version')->from('phinxlog')->execute()->rowCount(); 103 | $this->assertSame(4, $count); 104 | 105 | Migrator::migrate(); 106 | $count = $connection->selectQuery()->select('version')->from('phinxlog')->execute()->rowCount(); 107 | $this->assertSame(3, $count); 108 | } 109 | 110 | public function testMigrateWithoutTruncate(): void 111 | { 112 | $connection = ConnectionManager::get('test'); 113 | $cleaner = new SchemaCleaner(); 114 | $cleaner->drop('test'); 115 | 116 | $migrator = Migrator::migrate([], ['truncate' => false]); 117 | 118 | $count = $connection->selectQuery()->select('title')->from('articles')->execute()->rowCount(); 119 | $this->assertSame(1, $count); 120 | 121 | $migrator->truncate(); 122 | 123 | $count = $connection->selectQuery()->select('title')->from('articles')->execute()->rowCount(); 124 | $this->assertSame(0, $count); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/TestCase/SchemaCleanerDumpTest.php: -------------------------------------------------------------------------------- 1 | createSchemas(); 31 | $this->assertSame( 32 | 1, 33 | $connection->selectQuery()->select('id')->from('test_table')->execute()->rowCount() 34 | ); 35 | $this->assertSame( 36 | 1, 37 | $connection->selectQuery()->select('id')->from('test_table2')->execute()->rowCount() 38 | ); 39 | 40 | $this->expectException(PDOException::class); 41 | $connection->delete('test_table'); 42 | } 43 | 44 | public function testDropSchema() 45 | { 46 | $connection = ConnectionManager::get('test'); 47 | 48 | $this->createSchemas(); 49 | 50 | $tables = (new SchemaCleaner())->listTables($connection); 51 | 52 | // Assert that the schema is not empty 53 | $this->assertSame(['test_table', 'test_table2'], $tables, 'The schema should not be empty.'); 54 | 55 | // Drop the schema 56 | (new SchemaCleaner())->drop('test'); 57 | 58 | // Schema is empty 59 | $tables = (new SchemaCleaner())->listTables($connection); 60 | $this->assertSame([], $tables, 'The schema should be empty.'); 61 | } 62 | 63 | public function testTruncateSchema() 64 | { 65 | $connection = ConnectionManager::get('test'); 66 | 67 | $this->createSchemas(); 68 | 69 | (new SchemaCleaner())->truncate('test'); 70 | 71 | $this->assertSame( 72 | 0, 73 | $connection->selectQuery()->select('id')->from('test_table')->execute()->rowCount() 74 | ); 75 | $this->assertSame( 76 | 0, 77 | $connection->selectQuery()->select('id')->from('test_table2')->execute()->rowCount() 78 | ); 79 | } 80 | 81 | /** 82 | * Note that phinxlog tables are suffixed by _phinxlog. 83 | */ 84 | public function testUnsetMigrationTables(): void 85 | { 86 | $input = ['foo', 'phinxlog', 'phinxlog_bar', 'some_table', 'some_plugin_phinxlog']; 87 | $output = (new SchemaCleaner())->unsetMigrationTables($input); 88 | $this->assertSame(['foo', 'phinxlog_bar', 'some_table',], $output); 89 | } 90 | 91 | private function createSchemas() 92 | { 93 | $schemaCleaner = new SchemaCleaner(); 94 | $schemaCleaner->drop('test'); 95 | 96 | $connection = ConnectionManager::get('test'); 97 | 98 | $schema = new TableSchema('test_table'); 99 | $schema 100 | ->addColumn('id', 'integer') 101 | ->addColumn('name', 'string') 102 | ->addConstraint( 103 | 'primary', 104 | [ 105 | 'type' => TableSchema::CONSTRAINT_PRIMARY, 106 | 'columns' => ['id'], 107 | ] 108 | ); 109 | 110 | $queries = $schema->createSql($connection); 111 | foreach ($queries as $sql) { 112 | $connection->execute($sql); 113 | } 114 | 115 | $schema = new TableSchema('test_table2'); 116 | $schema 117 | ->addColumn('id', 'integer') 118 | ->addColumn('name', 'string') 119 | ->addColumn('fk_id', 'integer') 120 | ->addConstraint( 121 | 'primary', 122 | [ 123 | 'type' => TableSchema::CONSTRAINT_PRIMARY, 124 | 'columns' => ['id'], 125 | ] 126 | ) 127 | ->addConstraint( 128 | 'foreign_key', 129 | [ 130 | 'columns' => ['fk_id'], 131 | 'type' => TableSchema::CONSTRAINT_FOREIGN, 132 | 'references' => ['test_table', 'id', ], 133 | ] 134 | ); 135 | 136 | $queries = $schema->createSql($connection); 137 | 138 | foreach ($queries as $sql) { 139 | $connection->execute($sql); 140 | } 141 | 142 | $connection->insert('test_table', ['name' => 'foo']); 143 | 144 | $id = $connection->selectQuery()->select('id')->from('test_table')->limit(1)->execute()->fetch()[0]; 145 | $connection->insert('test_table2', ['name' => 'foo', 'fk_id' => $id]); 146 | 147 | $connection->execute($connection->getDriver()->enableForeignKeySQL()); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/TestCase/SchemaCleanerListTablesTest.php: -------------------------------------------------------------------------------- 1 | SchemaCleaner = new SchemaCleaner(); 33 | } 34 | 35 | public function tearDown(): void 36 | { 37 | unset($this->SchemaCleaner); 38 | parent::tearDown(); 39 | } 40 | 41 | /** 42 | * Check that the view has been created 43 | */ 44 | public function testThatTheViewExists() 45 | { 46 | $connection = ConnectionManager::get('test'); 47 | $result = $connection->execute('SELECT * FROM ' . self::VIEW_NAME)->fetch()[0]; 48 | $this->assertSame(self::VIEW_CONTENT, $result); 49 | } 50 | 51 | /** 52 | * Check that the view is ignored 53 | */ 54 | public function testThatTheViewIsIgnored() 55 | { 56 | $connection = ConnectionManager::get('test'); 57 | $allTables = $this->SchemaCleaner->listTables($connection); 58 | $this->assertSame(false, in_array(self::VIEW_NAME, $allTables)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/TestCase/SchemaCleanerTest.php: -------------------------------------------------------------------------------- 1 | Articles = TableRegistry::getTableLocator()->get('Articles'); 32 | } 33 | 34 | public function tearDown(): void 35 | { 36 | unset($this->Articles); 37 | } 38 | 39 | public function testDropSchema(): void 40 | { 41 | // Drop tables to ensure that the following migration runs 42 | (new SchemaCleaner())->drop('test'); 43 | 44 | // Populate the schema 45 | $migrations = new Migrations(); 46 | $migrations->migrate(['connection' => 'test']); 47 | $this->assertSame(1, $this->Articles->find()->count()); 48 | 49 | // Drop the schema 50 | (new SchemaCleaner())->drop('test'); 51 | 52 | $this->expectException(Exception::class); 53 | $this->Articles->find()->all(); 54 | } 55 | 56 | public function testTruncateSchema(): void 57 | { 58 | // Drop tables to ensure that the following migration runs 59 | (new SchemaCleaner())->drop('test'); 60 | // Populate the schema 61 | $migrations = new Migrations(['connection' => 'test']); 62 | $migrations->migrate(); 63 | $this->assertSame(1, $this->Articles->find()->count()); 64 | 65 | // Truncate the schema 66 | (new SchemaCleaner())->truncate('test'); 67 | $this->assertSame(0, $this->Articles->find()->count()); 68 | 69 | $migration = $migrations->status()[0]; 70 | $this->assertSame('up', $migration['status']); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/TestCase/TestConnectionManagerTest.php: -------------------------------------------------------------------------------- 1 | config()['database']; 26 | $this->assertSame('test_migrator', $testDB); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | parse() 24 | ->putenv(true) 25 | ->toEnv(true) 26 | ->toServer(true); 27 | } 28 | }; 29 | 30 | if (!getenv('DB_DRIVER')) { 31 | putenv('DB_DRIVER=Sqlite'); 32 | } 33 | $driver = getenv('DB_DRIVER'); 34 | 35 | if (file_exists(TESTS . ".env.$driver") && !file_exists(TESTS . '.env')) { 36 | copy(TESTS . ".env.$driver", TESTS . '.env'); 37 | } 38 | 39 | /** 40 | * Read .env file(s). 41 | */ 42 | $loadEnv(TESTS . '.env'); 43 | 44 | // Re-read the driver 45 | $driver = getenv('DB_DRIVER'); 46 | echo "Using driver $driver \n"; 47 | 48 | Configure::write('debug', true); 49 | Configure::write( 50 | 'App', 51 | [ 52 | 'namespace' => 'TestApp', 53 | 'paths' => [ 54 | 'plugins' => [TESTS . 'Plugins' . DS], 55 | ], 56 | ] 57 | ); 58 | 59 | $cacheConfig = [ 60 | 'className' => 'File', 61 | 'path' => CACHE, 62 | 'url' => env('CACHE_DEFAULT_URL', null), 63 | 'duration' => '+2 minutes', 64 | ]; 65 | 66 | Cache::setConfig('_cake_model_', $cacheConfig); 67 | Cache::setConfig('_cake_core_', $cacheConfig); 68 | 69 | $dbConnection = [ 70 | 'className' => 'Cake\Database\Connection', 71 | 'driver' => 'Cake\Database\Driver\\' . $driver, 72 | 'persistent' => false, 73 | 'host' => getenv('DB_HOST'), 74 | 'username' => getenv('DB_USER'), 75 | 'password' => getenv('DB_PWD'), 76 | 'database' => getenv('DB_DATABASE'), 77 | 'encoding' => 'utf8', 78 | 'timezone' => 'UTC', 79 | 'cacheMetadata' => true, 80 | 'quoteIdentifiers' => true, 81 | 'log' => false, 82 | //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], 83 | 'url' => env('DATABASE_TEST_URL', null), 84 | 'migrations' => [ 85 | ['source' => 'FooSource'], 86 | ['plugin' => 'FooPlugin'], 87 | ], 88 | ]; 89 | 90 | ConnectionManager::setConfig('test', $dbConnection); 91 | 92 | $dbDefaultConnection = $dbConnection; 93 | $dbDefaultConnection['database'] = 'migrator'; 94 | ConnectionManager::setConfig('default', $dbConnection); 95 | 96 | $dbConnection['migrations'] = ['plugin' => 'BarPlugin']; 97 | ConnectionManager::setConfig('test_2', $dbConnection); 98 | 99 | $dbConnection['migrations'] = true; 100 | ConnectionManager::setConfig('test_3', $dbConnection); 101 | --------------------------------------------------------------------------------