From 910dc1d2198ed718aa023b5f9d6096267fd761be Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 2 Jan 2026 20:57:11 +0100 Subject: [PATCH 1/2] Add --fake flag to seeds run and --seed option to seeds reset - Add --fake flag to mark seeds as executed without running them - Add --seed option to seeds reset for selective seed reset - Both features mirror similar functionality in migrations --- src/Command/SeedCommand.php | 13 ++- src/Command/SeedResetCommand.php | 25 ++++- src/Migration/Manager.php | 38 +++++-- tests/TestCase/Command/SeedCommandTest.php | 121 +++++++++++++++++++++ 4 files changed, 182 insertions(+), 15 deletions(-) diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php index b7562809..88e233b0 100644 --- a/src/Command/SeedCommand.php +++ b/src/Command/SeedCommand.php @@ -93,6 +93,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'short' => 'f', 'help' => 'Force re-running seeds that have already been executed', 'boolean' => true, + ]) + ->addOption('fake', [ + 'help' => 'Mark seeds as executed without actually running them', + 'boolean' => true, ]); return $parser; @@ -154,9 +158,14 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int $versionOrder = $config->getVersionOrder(); + $fake = (bool)$args->getOption('fake'); + if ($config->isDryRun()) { $io->info('DRY-RUN mode enabled'); } + if ($fake) { + $io->warning('performing fake seeding'); + } $io->verbose('using connection ' . (string)$args->getOption('connection')); $io->verbose('using paths ' . $config->getMigrationPath()); $io->verbose('ordering by ' . $versionOrder . ' time'); @@ -205,11 +214,11 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int } // run all the seed(ers) - $manager->seed(null, (bool)$args->getOption('force')); + $manager->seed(null, (bool)$args->getOption('force'), $fake); } else { // run seed(ers) specified as arguments foreach ($seeds as $seed) { - $manager->seed(trim($seed), (bool)$args->getOption('force')); + $manager->seed(trim($seed), (bool)$args->getOption('force'), $fake); } } $end = microtime(true); diff --git a/src/Command/SeedResetCommand.php b/src/Command/SeedResetCommand.php index 18ef30b1..f11964d0 100644 --- a/src/Command/SeedResetCommand.php +++ b/src/Command/SeedResetCommand.php @@ -49,8 +49,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'allowing seeds to be re-run without the --force flag.', '', 'seeds reset', + 'seeds reset --seed Users', + 'seeds reset --seed Users,Posts', 'seeds reset --plugin Demo', 'seeds reset -c secondary', + ])->addOption('seed', [ + 'help' => 'Comma-separated list of specific seeds to reset. Resets all seeds if not specified.', ])->addOption('plugin', [ 'short' => 'p', 'help' => 'The plugin to reset seeds for', @@ -100,9 +104,25 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $seeds = $manager->getSeeds(); $adapter = $manager->getEnvironment()->getAdapter(); - // Reset all seeds + // Filter seeds if --seed option is specified + $seedOption = $args->getOption('seed'); $seedsToReset = $seeds; + if ($seedOption) { + $requestedSeeds = array_map('trim', explode(',', (string)$seedOption)); + $seedsToReset = []; + + foreach ($requestedSeeds as $requestedSeed) { + $normalizedName = $manager->normalizeSeedName($requestedSeed, $seeds); + if ($normalizedName === null) { + $io->error("Seed `{$requestedSeed}` does not exist."); + + return self::CODE_ERROR; + } + $seedsToReset[$normalizedName] = $seeds[$normalizedName]; + } + } + if (empty($seedsToReset)) { $io->warning('No seeds to reset.'); @@ -111,7 +131,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int // Show what will be reset and ask for confirmation $io->out(''); - $io->out('All seeds will be reset:'); + $resetAllMessage = $seedOption ? 'The following seeds will be reset:' : 'All seeds will be reset:'; + $io->out($resetAllMessage); foreach ($seedsToReset as $seed) { $io->out(' - ' . Util::getSeedDisplayName($seed->getName())); } diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 963bd57f..bbdce355 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -540,9 +540,10 @@ public function executeMigration(MigrationInterface $migration, string $directio * * @param \Migrations\SeedInterface $seed Seed * @param bool $force Force re-execution even if seed has already been executed + * @param bool $fake Record seed as executed without actually running it * @return void */ - public function executeSeed(SeedInterface $seed, bool $force = false): void + public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake = false): void { $this->getIo()->out(''); @@ -560,6 +561,26 @@ public function executeSeed(SeedInterface $seed, bool $force = false): void return; } + // Ensure seed schema table exists + $adapter = $this->getEnvironment()->getAdapter(); + if (!$adapter->hasTable($adapter->getSeedSchemaTableName())) { + $adapter->createSeedSchemaTable(); + } + + if ($fake) { + // Record seed as executed without running it + $this->printSeedStatus($seed, 'faking'); + + if (!$seed->isIdempotent()) { + $executedTime = date('Y-m-d H:i:s'); + $adapter->seedExecuted($seed, $executedTime); + } + + $this->printSeedStatus($seed, 'faked'); + + return; + } + // Auto-execute missing dependencies $missingDeps = $this->getSeedDependenciesNotExecuted($seed); if (!empty($missingDeps)) { @@ -568,18 +589,12 @@ public function executeSeed(SeedInterface $seed, bool $force = false): void ' Auto-executing dependency: %s', $depSeed->getName(), )); - $this->executeSeed($depSeed, $force); + $this->executeSeed($depSeed, $force, $fake); } } $this->printSeedStatus($seed, 'seeding'); - // Ensure seed schema table exists - $adapter = $this->getEnvironment()->getAdapter(); - if (!$adapter->hasTable($adapter->getSeedSchemaTableName())) { - $adapter->createSeedSchemaTable(); - } - // Execute the seeder and log the time elapsed. $start = microtime(true); $this->getEnvironment()->executeSeed($seed); @@ -794,10 +809,11 @@ public function rollback(int|string|null $target = null, bool $force = false, bo * * @param string|null $seed Seeder * @param bool $force Force re-execution even if seed has already been executed + * @param bool $fake Record seed as executed without actually running it * @throws \InvalidArgumentException * @return void */ - public function seed(?string $seed = null, bool $force = false): void + public function seed(?string $seed = null, bool $force = false, bool $fake = false): void { $seeds = $this->getSeeds(); @@ -805,14 +821,14 @@ public function seed(?string $seed = null, bool $force = false): void // run all seeders foreach ($seeds as $seeder) { if (array_key_exists($seeder->getName(), $seeds)) { - $this->executeSeed($seeder, $force); + $this->executeSeed($seeder, $force, $fake); } } } else { // run only one seeder $normalizedName = $this->normalizeSeedName($seed, $seeds); if ($normalizedName !== null) { - $this->executeSeed($seeds[$normalizedName], $force); + $this->executeSeed($seeds[$normalizedName], $force, $fake); } else { throw new InvalidArgumentException(sprintf('The seed `%s` does not exist', $seed)); } diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php index 77935ae4..1522f8f9 100644 --- a/tests/TestCase/Command/SeedCommandTest.php +++ b/tests/TestCase/Command/SeedCommandTest.php @@ -570,4 +570,125 @@ public function testNonIdempotentSeedIsTracked(): void $this->assertOutputContains('already executed'); $this->assertOutputNotContains('seeding'); } + + public function testFakeSeedMarksAsExecuted(): void + { + $this->createTables(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + + // Run with --fake flag + $this->exec('seeds run -c test NumbersSeed --fake'); + $this->assertExitSuccess(); + $this->assertErrorContains('performing fake seeding'); + $this->assertOutputContains('faking'); + $this->assertOutputContains('faked'); + $this->assertOutputNotContains('seeding'); + + // Verify NO data was inserted + $query = $connection->execute('SELECT COUNT(*) FROM numbers'); + $this->assertEquals(0, $query->fetchColumn(0), 'Fake seed should not insert data'); + + // Verify the seed WAS tracked in cake_seeds table + $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $this->assertEquals(1, $seedLog->fetchColumn(0), 'Fake seeds should be tracked'); + + // Running again should show already executed + $this->exec('seeds run -c test NumbersSeed'); + $this->assertExitSuccess(); + $this->assertOutputContains('already executed'); + } + + public function testFakeSeedWithForce(): void + { + $this->createTables(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + + // Run with --fake first + $this->exec('seeds run -c test NumbersSeed --fake'); + $this->assertExitSuccess(); + + // Verify seed is tracked + $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $this->assertEquals(1, $seedLog->fetchColumn(0)); + + // Run with --force to actually execute it + $this->exec('seeds run -c test NumbersSeed --force'); + $this->assertExitSuccess(); + $this->assertOutputContains('seeding'); + + // Verify data was inserted + $query = $connection->execute('SELECT COUNT(*) FROM numbers'); + $this->assertEquals(1, $query->fetchColumn(0)); + } + + public function testResetSpecificSeed(): void + { + $this->createTables(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + + // Run two seeds + $this->exec('seeds run -c test NumbersSeed'); + $this->assertExitSuccess(); + + $this->exec('seeds run -c test StoresSeed'); + $this->assertExitSuccess(); + + // Verify both are tracked + $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $this->assertEquals(1, $numbersLog->fetchColumn(0)); + + $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\''); + $this->assertEquals(1, $storesLog->fetchColumn(0)); + + // Reset only Numbers seed + $this->exec('seeds reset -c test --seed Numbers', ['y']); + $this->assertExitSuccess(); + $this->assertOutputContains('The following seeds will be reset:'); + $this->assertOutputNotContains('All seeds will be reset:'); + + // Verify Numbers is reset but Stores is still tracked + $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $this->assertEquals(0, $numbersLog->fetchColumn(0), 'Numbers seed should be reset'); + + $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\''); + $this->assertEquals(1, $storesLog->fetchColumn(0), 'Stores seed should still be tracked'); + } + + public function testResetMultipleSpecificSeeds(): void + { + $this->createTables(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + + // Run seeds + $this->exec('seeds run -c test NumbersSeed'); + $this->exec('seeds run -c test StoresSeed'); + + // Reset both with comma-separated list + $this->exec('seeds reset -c test --seed Numbers,Stores', ['y']); + $this->assertExitSuccess(); + + // Verify both are reset + $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $this->assertEquals(0, $numbersLog->fetchColumn(0)); + + $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\''); + $this->assertEquals(0, $storesLog->fetchColumn(0)); + } + + public function testResetNonExistentSeed(): void + { + $this->createTables(); + + $this->exec('seeds reset -c test --seed NonExistent'); + $this->assertExitError(); + $this->assertErrorContains('Seed `NonExistent` does not exist'); + } } From 39b297dcbd526576456614e83a05c7114032865f Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 05:31:45 +0100 Subject: [PATCH 2/2] Show clear message when faking idempotent seeds Idempotent seeds are not tracked, so faking them doesn't make sense. Instead of showing misleading "faking"/"faked" messages, now shows "skipped (idempotent)" to clarify what's happening. --- src/Migration/Manager.php | 13 +++++++++---- tests/TestCase/Command/SeedCommandTest.php | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index bbdce355..2ce00a17 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -568,13 +568,18 @@ public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake } if ($fake) { + // Idempotent seeds are not tracked, so faking doesn't apply + if ($seed->isIdempotent()) { + $this->printSeedStatus($seed, 'skipped (idempotent)'); + + return; + } + // Record seed as executed without running it $this->printSeedStatus($seed, 'faking'); - if (!$seed->isIdempotent()) { - $executedTime = date('Y-m-d H:i:s'); - $adapter->seedExecuted($seed, $executedTime); - } + $executedTime = date('Y-m-d H:i:s'); + $adapter->seedExecuted($seed, $executedTime); $this->printSeedStatus($seed, 'faked'); diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php index 1522f8f9..ac240171 100644 --- a/tests/TestCase/Command/SeedCommandTest.php +++ b/tests/TestCase/Command/SeedCommandTest.php @@ -691,4 +691,23 @@ public function testResetNonExistentSeed(): void $this->assertExitError(); $this->assertErrorContains('Seed `NonExistent` does not exist'); } + + public function testFakeIdempotentSeedIsSkipped(): void + { + $this->createTables(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + + // Run idempotent seed with --fake flag + $this->exec('seeds run -c test -s TestSeeds IdempotentTest --fake'); + $this->assertExitSuccess(); + $this->assertOutputContains('skipped (idempotent)'); + $this->assertOutputNotContains('faking'); + $this->assertOutputNotContains('faked'); + + // Verify the seed was NOT tracked (idempotent seeds are never tracked) + $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\''); + $this->assertEquals(0, $seedLog->fetchColumn(0), 'Idempotent seeds should not be tracked even when faked'); + } }