diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml new file mode 100644 index 0000000..2d0c75f --- /dev/null +++ b/.github/workflows/ci-mysql.yml @@ -0,0 +1,65 @@ +name: MySQL + +on: + push: + branches: [ 2.0 ] + pull_request: + branches: [ 2.0 ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest ] + php: [ 8.1 ] + mysql: [ 8.0 ] + stability: [ prefer-lowest, prefer-stable ] + + name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} + + services: + mysql: + image: mysql:${{ matrix.mysql }} + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: spiral + MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password + ports: + - 13306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Validate Composer + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Restore Composer Cache + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.stability }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.stability }}-composer + + - name: Install Dependencies + uses: nick-invision/retry@v2.7.0 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit --group driver-mysql diff --git a/.github/workflows/ci-sqlite.yml b/.github/workflows/ci-sqlite.yml new file mode 100644 index 0000000..c28a961 --- /dev/null +++ b/.github/workflows/ci-sqlite.yml @@ -0,0 +1,53 @@ +name: SQLite + +on: + push: + branches: [ 2.0 ] + pull_request: + branches: [ 2.0 ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest ] + php: [ 8.1 ] + stability: [ prefer-lowest, prefer-stable ] + + name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Validate Composer + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Restore Composer Cache + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.stability }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.stability }}-composer + + - name: Install Dependencies + uses: nick-invision/retry@v2.7.0 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit --group driver-sqlite diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 59537eb..9cfc9f4 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -2,9 +2,9 @@ name: run-tests on: push: - branches: [ master ] + branches: [ 2.0 ] pull_request: - branches: [ master ] + branches: [ 2.0 ] jobs: test: @@ -13,11 +13,23 @@ jobs: fail-fast: true matrix: os: [ ubuntu-latest ] - php: [ 8.0, 8.1 ] + php: [ 8.1 ] + mysql: [ 8.0 ] stability: [ prefer-lowest, prefer-stable ] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} + services: + mysql: + image: mysql:${{ matrix.mysql }} + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: spiral + MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password + ports: + - 13306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index ab7e4d5..1e751e2 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -2,9 +2,9 @@ name: run-tests on: push: - branches: [ master ] + branches: [ 2.0 ] pull_request: - branches: [ master ] + branches: [ 2.0 ] jobs: static-analysis: @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ 8.0 ] + php: [ 8.1 ] os: [ ubuntu-latest ] steps: diff --git a/composer.json b/composer.json index 8584a3e..3db23ad 100644 --- a/composer.json +++ b/composer.json @@ -1,65 +1,73 @@ { - "name": "spiral-packages/database-seeder", - "description": "The package provides the ability to seed your database with data using seed classes", - "keywords": [ - "spiral-packages", - "spiral", - "seeder", - "database-seeder" - ], - "homepage": "https://github.com/spiral-packages/database-seeder", - "license": "MIT", - "authors": [ - { - "name": "Maxim Smakouz", - "email": "m.smakouz@gmail.com", - "role": "Developer" - } - ], - "require": { - "php": "^8.1", - "cycle/orm": "^2.1", - "butschster/entity-faker": "^0.9", - "fakerphp/faker": "^1.19", - "laminas/laminas-hydrator": "^4.3", - "spiral/attributes": "^3.0", - "spiral/scaffolder": "^3.0", - "doctrine/annotations": "^1.13", - "spiral/boot": "^3.0", - "spiral/console": "^3.0" - }, - "require-dev": { - "spiral/framework": "^3.0", - "mockery/mockery": "^1.5", - "phpunit/phpunit": "^9.5", - "spiral/testing": "^2.0", - "vimeo/psalm": "^4.20" - }, - "autoload": { - "psr-4": { - "Spiral\\DatabaseSeeder\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Tests\\App\\": "tests/app", - "Tests\\": "tests/src" - } - }, - "scripts": { - "test": "vendor/bin/phpunit", - "psalm": "vendor/bin/psalm --config=psalm.xml ./src" - }, - "config": { - "sort-packages": true - }, - "extra": { - "spiral": { - "bootloaders": [ - "Spiral\\DatabaseSeeder\\Bootloader\\DatabaseSeederBootloader" - ] - } - }, - "minimum-stability": "dev", - "prefer-stable": true + "name": "spiral-packages/database-seeder", + "description": "The package provides the ability to seed your database with data using seed classes", + "keywords": [ + "spiral-packages", + "spiral", + "seeder", + "database-seeder" + ], + "homepage": "https://github.com/spiral-packages/database-seeder", + "license": "MIT", + "authors": [ + { + "name": "Maxim Smakouz", + "email": "m.smakouz@gmail.com", + "role": "Developer" + } + ], + "require": { + "php": "^8.1", + "butschster/entity-faker": "^0.9", + "fakerphp/faker": "^1.19", + "laminas/laminas-hydrator": "^4.3", + "spiral/attributes": "^3.0", + "spiral/scaffolder": "^3.0", + "doctrine/annotations": "^1.13", + "spiral/boot": "^3.0", + "spiral/console": "^3.0" + }, + "require-dev": { + "spiral/framework": "^3.0", + "mockery/mockery": "^1.5", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.20", + "spiral/cycle-bridge": "^2.0", + "spiral/testing": "^2.0" + }, + "autoload": { + "files": [ + "src/polyfill.php" + ], + "psr-4": { + "Spiral\\DatabaseSeeder\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\App\\": "tests/app/src", + "Tests\\Database\\": "tests/app/database", + "Tests\\": "tests/src" + } + }, + "suggest": { + "spiral/testing": "To use the Spiral\\DatabaseSeeder\\TestCase class and helpers to test an app with DB", + "spiral/cycle-bridge": "For easy database and ORM configuration in a test application" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "psalm": "vendor/bin/psalm --config=psalm.xml ./src" + }, + "config": { + "sort-packages": true + }, + "extra": { + "spiral": { + "bootloaders": [ + "Spiral\\DatabaseSeeder\\Bootloader\\DatabaseSeederBootloader" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/src/Database/DatabaseState.php b/src/Database/DatabaseState.php new file mode 100644 index 0000000..f96b080 --- /dev/null +++ b/src/Database/DatabaseState.php @@ -0,0 +1,10 @@ +getConfig('migration'); + if (empty($config['directory'])) { + throw new DatabaseMigrationsException( + 'Please, configure migrations in your test application to use DatabaseMigrations.' + ); + } + + if (!isset($config['safe']) || $config['safe'] !== true) { + throw new DatabaseMigrationsException( + 'The `safe` parameter in the test application migrations configuration must be set to true.' + ); + } + + $this->runCommand('cycle:migrate', ['--run' => true]); + + $self = $this; + $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use($self, $config) { + $self->runCommand('migrate:rollback', ['--all' => true]); + + $self->cleanupDirectories($config['directory']); + + DatabaseState::$migrated = false; + }); + } +} diff --git a/src/Database/Traits/RefreshDatabase.php b/src/Database/Traits/RefreshDatabase.php new file mode 100644 index 0000000..ba0e252 --- /dev/null +++ b/src/Database/Traits/RefreshDatabase.php @@ -0,0 +1,93 @@ +beforeRefreshingDatabase(); + + $this->usingInMemoryDatabase() + ? $this->refreshInMemoryDatabase() + : $this->refreshTestDatabase(); + + $this->afterRefreshingDatabase(); + } + + /** + * Begin a database transaction on the testing database. + */ + public function beginDatabaseTransaction(): void + { + $driver = $this->getContainer()->get(Database::class)->getDriver(); + $driver->beginTransaction(); + + $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use($driver) { + while ($driver->getTransactionLevel() >= 1) { + $driver->rollbackTransaction(); + } + $driver->disconnect(); + }); + } + + /** + * Refresh the in-memory database. + */ + protected function refreshInMemoryDatabase(): void + { + $this->runCommand('cycle:sync'); + } + + /** + * Refresh a conventional test database. + */ + protected function refreshTestDatabase(): void + { + if (!DatabaseState::$migrated) { + $this->runCommand('cycle:sync'); + + DatabaseState::$migrated = true; + } + + $this->beginDatabaseTransaction(); + } + + /** + * Determine if an in-memory database is being used. + */ + protected function usingInMemoryDatabase(): bool + { + $manager = $this->getContainer()->get(DatabaseManager::class); + $info = $manager->database()->getDriver()->__debugInfo(); + + return isset($info['connection']) && $info['connection'] instanceof MemoryConnectionConfig; + } + + /** + * Perform any work that should take place before the database has started refreshing. + */ + protected function beforeRefreshingDatabase(): void + { + // ... + } + + /** + * Perform any work that should take place once the database has finished refreshing. + */ + protected function afterRefreshingDatabase(): void + { + // ... + } +} diff --git a/src/TestCase.php b/src/TestCase.php new file mode 100644 index 0000000..f3f4492 --- /dev/null +++ b/src/TestCase.php @@ -0,0 +1,28 @@ +setUpTraits(); + } + + private function setUpTraits(): void + { + /** @see \Spiral\DatabaseSeeder\Database\Traits\RefreshDatabase */ + if (\method_exists($this, 'refreshDatabase')) { + $this->refreshDatabase(); + } + + /** @see \Spiral\DatabaseSeeder\Database\Traits\DatabaseMigrations */ + if (\method_exists($this, 'runDatabaseMigrations')) { + $this->runDatabaseMigrations(); + } + } +} diff --git a/src/polyfill.php b/src/polyfill.php new file mode 100644 index 0000000..00f9d1c --- /dev/null +++ b/src/polyfill.php @@ -0,0 +1,11 @@ + env('DEFAULT_DB'), + 'databases' => [ + 'sqlite' => [ + 'driver' => 'sqlite', + ], + 'mysql' => [ + 'driver' => 'mysql', + ], + ], + 'drivers' => [ + 'sqlite' => new Config\SQLiteDriverConfig( + connection: new Config\SQLite\MemoryConnectionConfig(), + queryCache: true + ), + 'mysql' => new Config\MySQLDriverConfig( + connection: new Config\MySQL\TcpConnectionConfig( + database: 'spiral', + host: '127.0.0.1', + port: 13306, + user: 'root', + password: 'root', + ), + queryCache: true + ), + ], +]; diff --git a/tests/app/config/migration.php b/tests/app/config/migration.php new file mode 100644 index 0000000..96027ba --- /dev/null +++ b/tests/app/config/migration.php @@ -0,0 +1,9 @@ + directory('app') . 'database/migrations/', + 'table' => 'migrations', + 'safe' => true, +]; diff --git a/tests/src/Fixture/Factory/CommentFactory.php b/tests/app/database/Factory/CommentFactory.php similarity index 88% rename from tests/src/Fixture/Factory/CommentFactory.php rename to tests/app/database/Factory/CommentFactory.php index 54c0a26..06dae82 100644 --- a/tests/src/Fixture/Factory/CommentFactory.php +++ b/tests/app/database/Factory/CommentFactory.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Fixture\Factory; +namespace Tests\Database\Factory; use Spiral\DatabaseSeeder\Factory\AbstractFactory; -use Tests\Fixture\Entity\Comment; +use Tests\App\Database\Comment; class CommentFactory extends AbstractFactory { diff --git a/tests/src/Fixture/Factory/PostFactory.php b/tests/app/database/Factory/PostFactory.php similarity index 89% rename from tests/src/Fixture/Factory/PostFactory.php rename to tests/app/database/Factory/PostFactory.php index df44bbc..bb54d07 100644 --- a/tests/src/Fixture/Factory/PostFactory.php +++ b/tests/app/database/Factory/PostFactory.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Fixture\Factory; +namespace Tests\Database\Factory; use Spiral\DatabaseSeeder\Factory\AbstractFactory; -use Tests\Fixture\Entity\Post; +use Tests\App\Database\Post; class PostFactory extends AbstractFactory { diff --git a/tests/src/Fixture/Factory/UserFactory.php b/tests/app/database/Factory/UserFactory.php similarity index 68% rename from tests/src/Fixture/Factory/UserFactory.php rename to tests/app/database/Factory/UserFactory.php index e8a5536..43d5b40 100644 --- a/tests/src/Fixture/Factory/UserFactory.php +++ b/tests/app/database/Factory/UserFactory.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Fixture\Factory; +namespace Tests\Database\Factory; use Spiral\DatabaseSeeder\Factory\AbstractFactory; -use Tests\Fixture\Entity\User; +use Tests\App\Database\User; class UserFactory extends AbstractFactory { @@ -20,6 +20,9 @@ public function definition(): array 'firstName' => $this->faker->firstName(), 'lastName' => $this->faker->lastName(), 'birthday' => \DateTimeImmutable::createFromMutable($this->faker->dateTime()), + 'age' => $this->faker->numberBetween(1, 90), + 'active' => $this->faker->boolean, + 'someFloatVal' => $this->faker->randomFloat() ]; } } diff --git a/tests/src/Fixture/Seeder/CommentSeeder.php b/tests/app/database/Seeder/CommentSeeder.php similarity index 68% rename from tests/src/Fixture/Seeder/CommentSeeder.php rename to tests/app/database/Seeder/CommentSeeder.php index c643f01..418d772 100644 --- a/tests/src/Fixture/Seeder/CommentSeeder.php +++ b/tests/app/database/Seeder/CommentSeeder.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace Tests\Fixture\Seeder; +namespace Tests\Database\Seeder; use Spiral\DatabaseSeeder\Attribute\Seeder; use Spiral\DatabaseSeeder\Seeder\AbstractSeeder; -use Tests\Fixture\Entity\Comment; -use Tests\Fixture\Entity\Post; -use Tests\Fixture\Entity\User; -use Tests\Fixture\Factory\PostFactory; -use Tests\Fixture\Factory\UserFactory; +use Tests\App\Database\Comment; +use Tests\App\Database\Post; +use Tests\App\Database\User; +use Tests\Database\Factory\PostFactory; +use Tests\Database\Factory\UserFactory; #[Seeder] class CommentSeeder extends AbstractSeeder @@ -20,7 +20,7 @@ public function run(): \Generator /** @var Post $post */ $post = PostFactory::new()->createOne(); /** @var User $user */ - $user= UserFactory::new()->createOne(); + $user = UserFactory::new()->createOne(); $comment = new Comment(); $comment->post = $post; diff --git a/tests/src/Fixture/Seeder/PostSeeder.php b/tests/app/database/Seeder/PostSeeder.php similarity index 80% rename from tests/src/Fixture/Seeder/PostSeeder.php rename to tests/app/database/Seeder/PostSeeder.php index 24731b1..9d22b85 100644 --- a/tests/src/Fixture/Seeder/PostSeeder.php +++ b/tests/app/database/Seeder/PostSeeder.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Tests\Fixture\Seeder; +namespace Tests\Database\Seeder; use Spiral\DatabaseSeeder\Attribute\Seeder; use Spiral\DatabaseSeeder\Seeder\AbstractSeeder; -use Tests\Fixture\Factory\PostFactory; +use Tests\Database\Factory\PostFactory; #[Seeder(priority: 3)] class PostSeeder extends AbstractSeeder diff --git a/tests/src/Fixture/Seeder/UserSeeder.php b/tests/app/database/Seeder/UserSeeder.php similarity index 78% rename from tests/src/Fixture/Seeder/UserSeeder.php rename to tests/app/database/Seeder/UserSeeder.php index fc513e0..9d24eb7 100644 --- a/tests/src/Fixture/Seeder/UserSeeder.php +++ b/tests/app/database/Seeder/UserSeeder.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Fixture\Seeder; +namespace Tests\Database\Seeder; use Spiral\DatabaseSeeder\Seeder\AbstractSeeder; -use Tests\Fixture\Factory\UserFactory; +use Tests\Database\Factory\UserFactory; class UserSeeder extends AbstractSeeder { diff --git a/tests/src/Fixture/Seeder/WrongSeeder.php b/tests/app/database/Seeder/WrongSeeder.php similarity index 77% rename from tests/src/Fixture/Seeder/WrongSeeder.php rename to tests/app/database/Seeder/WrongSeeder.php index c49ded3..ba0e548 100644 --- a/tests/src/Fixture/Seeder/WrongSeeder.php +++ b/tests/app/database/Seeder/WrongSeeder.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Fixture\Seeder; +namespace Tests\Database\Seeder; use Spiral\DatabaseSeeder\Attribute\Seeder; diff --git a/tests/app/src/Database/Comment.php b/tests/app/src/Database/Comment.php new file mode 100644 index 0000000..83e6b64 --- /dev/null +++ b/tests/app/src/Database/Comment.php @@ -0,0 +1,28 @@ +refreshApp(); + } +} diff --git a/tests/src/Functional/Driver/MySQL/Database/Traits/RefreshDatabaseTest.php b/tests/src/Functional/Driver/MySQL/Database/Traits/RefreshDatabaseTest.php new file mode 100644 index 0000000..622b5f0 --- /dev/null +++ b/tests/src/Functional/Driver/MySQL/Database/Traits/RefreshDatabaseTest.php @@ -0,0 +1,43 @@ + 'mysql' + ]; + + public function testUsingInMemoryDatabase(): void + { + $this->assertFalse($this->usingInMemoryDatabase()); + } + + public function testRefreshTestDatabase(): void + { + $this->assertSame(0, $this->getContainer()->get(Database::class)->getDriver()->getTransactionLevel()); + + $this->assertFalse(DatabaseState::$migrated); + $this->refreshTestDatabase(); + $this->assertTrue(DatabaseState::$migrated); + + // the transaction is opened. It will be closed in the finalizer after executing a test + $this->assertSame(1, $this->getContainer()->get(Database::class)->getDriver()->getTransactionLevel()); + + $this->assertTableExists('comments'); + $this->assertTableExists('posts'); + $this->assertTableExists('users'); + } +} diff --git a/tests/src/Functional/Driver/SQLite/Database/Traits/RefreshDatabaseTest.php b/tests/src/Functional/Driver/SQLite/Database/Traits/RefreshDatabaseTest.php new file mode 100644 index 0000000..cbd9da8 --- /dev/null +++ b/tests/src/Functional/Driver/SQLite/Database/Traits/RefreshDatabaseTest.php @@ -0,0 +1,35 @@ +assertTrue($this->usingInMemoryDatabase()); + } + + public function testRefreshInMemoryDatabase(): void + { + // by default, in memory db empty + $this->assertTableIsNotExists('comments'); + $this->assertTableIsNotExists('posts'); + $this->assertTableIsNotExists('users'); + + $this->refreshInMemoryDatabase(); + + $this->assertTableExists('comments'); + $this->assertTableExists('posts'); + $this->assertTableExists('users'); + } +} diff --git a/tests/src/Functional/TestCase.php b/tests/src/Functional/TestCase.php new file mode 100644 index 0000000..e258719 --- /dev/null +++ b/tests/src/Functional/TestCase.php @@ -0,0 +1,70 @@ + 'sqlite' + ]; + + protected function tearDown(): void + { + parent::tearDown(); + + $this->cleanUpRuntimeDirectory(); + } + + public function rootDirectory(): string + { + return \dirname(__DIR__, 2); + } + + public function defineDirectories(string $root): array + { + return [ + 'root' => $root, + 'app' => $root . '/app', + 'runtime' => $root . '/app/runtime', + 'cache' => $root . '/app/runtime/cache', + 'config' => $root . '/app/config', + ]; + } + + public function defineBootloaders(): array + { + return [ + ConfigurationBootloader::class, + CycleOrmBridge\DatabaseBootloader::class, + CycleOrmBridge\MigrationsBootloader::class, + CycleOrmBridge\SchemaBootloader::class, + CycleOrmBridge\CycleOrmBootloader::class, + CycleOrmBridge\AnnotatedBootloader::class, + CycleOrmBridge\CommandBootloader::class, + DatabaseSeederBootloader::class, + ]; + } + + public function assertTableExists(string $table): void + { + static::assertTrue( + $this->getContainer()->get(Database::class)->hasTable($table), + \sprintf('Table [%s] does not exist.', $table) + ); + } + + public function assertTableIsNotExists(string $table): void + { + static::assertFalse( + $this->getContainer()->get(Database::class)->hasTable($table), + \sprintf('Table [%s] exists.', $table) + ); + } +} diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php deleted file mode 100644 index 5e4ab5d..0000000 --- a/tests/src/TestCase.php +++ /dev/null @@ -1,23 +0,0 @@ -