Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/Command/SeedCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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('<info>using connection</info> ' . (string)$args->getOption('connection'));
$io->verbose('<info>using paths</info> ' . $config->getMigrationPath());
$io->verbose('<info>ordering by</info> ' . $versionOrder . ' time');
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 23 additions & 2 deletions src/Command/SeedResetCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'allowing seeds to be re-run without the --force flag.',
'',
'<info>seeds reset</info>',
'<info>seeds reset --seed Users</info>',
'<info>seeds reset --seed Users,Posts</info>',
'<info>seeds reset --plugin Demo</info>',
'<info>seeds reset -c secondary</info>',
])->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',
Expand Down Expand Up @@ -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.');

Expand All @@ -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('<info>All seeds will be reset:</info>');
$resetAllMessage = $seedOption ? '<info>The following seeds will be reset:</info>' : '<info>All seeds will be reset:</info>';
$io->out($resetAllMessage);
foreach ($seedsToReset as $seed) {
$io->out(' - ' . Util::getSeedDisplayName($seed->getName()));
}
Expand Down
43 changes: 32 additions & 11 deletions src/Migration/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('');

Expand All @@ -560,6 +561,31 @@ 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) {
// 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');

$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)) {
Expand All @@ -568,18 +594,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);
Expand Down Expand Up @@ -794,25 +814,26 @@ 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();

if ($seed === null) {
// 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));
}
Expand Down
140 changes: 140 additions & 0 deletions tests/TestCase/Command/SeedCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -570,4 +570,144 @@ 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');
}

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');
}
}