diff --git a/features/testing.feature b/features/testing.feature index 022fdda3..bbab3a22 100644 --- a/features/testing.feature +++ b/features/testing.feature @@ -37,6 +37,41 @@ Feature: Test that WP-CLI loads. sqlite """ + Scenario: Leave files a scenario created before installing out of the installation cache + Given an empty directory + And a scenario-files/wp-cli.yml file: + """ + user: admin + """ + And a WP installation in 'scenario-files/' + Then the scenario-files/wp-cli.yml file should exist + + When I run `php -r "rename('scenario-files', 'first');"` + + # Reuse the subdirectory to select the same installation cache. + Given a WP installation in 'scenario-files/' + Then the scenario-files/wp-config.php file should exist + And the scenario-files/wp-cli.yml file should not exist + + @skip-windows + Scenario: Leave what a scenario linked to before installing out of the installation cache + Given an empty directory + And a linked-files/wp-cli.yml file: + """ + user: admin + """ + When I run `php -r "mkdir('scenario-links'); symlink(getcwd() . '/linked-files', 'scenario-links/linked');"` + + Given a WP installation in 'scenario-links' + Then the scenario-links/linked/wp-cli.yml file should exist + + When I run `php -r "rename('scenario-links', 'first');"` + + # Reuse the subdirectory to select the same installation cache. + Given a WP installation in 'scenario-links' + Then the scenario-links/wp-config.php file should exist + And the scenario-links/linked directory should not exist + @require-sqlite Scenario: Restore independent SQLite databases from the installation cache Given a WP installation in 'cached' diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index ba8ae9f5..cbc2e464 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1843,7 +1843,8 @@ public function download_wp( $subdir = '', $version = '' ): void { $dest_dir = rtrim( $this->variables['RUN_DIR'], '/\\' ) . ( $subdir ? DIRECTORY_SEPARATOR . $subdir : '' ); - if ( $subdir ) { + // The scenario may have created files in the subdirectory already. + if ( $subdir && ! is_dir( $dest_dir ) ) { mkdir( $dest_dir, 0777, true /*recursive*/ ); } @@ -1937,6 +1938,11 @@ public function install_wp( $subdir = '', $version = '' ): void { $this->create_db(); } $this->create_run_dir(); + + // Files the scenario created before installing belong to that scenario only, and must not + // end up in the installation cache that later scenarios are set up from. + $scenario_files = self::list_dir( '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR'] ); + $this->download_wp( $subdir, $version ); $this->create_config( $subdir, $config_extra_php ); @@ -1969,6 +1975,7 @@ public function install_wp( $subdir = '', $version = '' ): void { } self::dir_diff_copy( $run_dir, self::$cache_dir, $install_cache_path ); + self::remove_from_dir( $install_cache_path, $scenario_files ); if ( 'sqlite' !== self::$db_type ) { $mysqldump_binary = Utils\get_sql_dump_command(); @@ -2239,6 +2246,61 @@ private static function log_run_times_after_scenario( $scope ): void { } } + /** + * List the files and directories within a directory, relative to it. + * + * Symbolic links are listed, but not followed. + * + * @param string $dir + * @return array Whether each entry is a symbolic link, keyed by relative path, directories after + * their contents. Empty if the directory does not exist. + */ + private static function list_dir( $dir ): array { + $dir = rtrim( $dir, '/\\' ); + if ( ! is_dir( $dir ) ) { + return []; + } + + $paths = []; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + /** + * @var \SplFileInfo $item + */ + foreach ( $iterator as $item ) { + $paths[ substr( $item->getPathname(), strlen( $dir ) + 1 ) ] = $item->isLink(); + } + + return $paths; + } + + /** + * Remove the given files from a directory, as well as the given directories once they are empty. + * + * A copy of what a symbolic link pointed to is removed with all its contents. + * + * @param string $dir + * @param array $paths Whether each entry was a symbolic link, keyed by relative path, directories + * after their contents. + */ + private static function remove_from_dir( $dir, array $paths ): void { + foreach ( $paths as $path => $was_link ) { + $full_path = "{$dir}/{$path}"; + + if ( $was_link && is_dir( $full_path ) && ! is_link( $full_path ) ) { + self::remove_dir( $full_path ); + } elseif ( is_dir( $full_path ) && ! is_link( $full_path ) ) { + // Only succeeds for directories that are empty by now. + @rmdir( $full_path ); + } elseif ( file_exists( $full_path ) || is_link( $full_path ) ) { + unlink( $full_path ); + } + } + } + /** * Copy files in updated directory that are not in source directory to copy directory. ("Incremental backup".) * Note: does not deal with changed files (ie does not compare file contents for changes), for speed reasons.