diff --git a/src/commands/mysql.php b/src/commands/mysql.php index 2e76f1a..b724537 100644 --- a/src/commands/mysql.php +++ b/src/commands/mysql.php @@ -21,6 +21,8 @@ {$cli_name} {$subcommand} HELP; + echo colorize( $help ); + return; } diff --git a/tests/Cli/AirplaneModeTest.php b/tests/Cli/AirplaneModeTest.php new file mode 100644 index 0000000..020080c --- /dev/null +++ b/tests/Cli/AirplaneModeTest.php @@ -0,0 +1,55 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'airplane-mode on', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Airplane mode plugin installed', + $output, + 'Turning airplane mode on should confirm the plugin was installed.' + ); + $this->assertStringNotContainsString( + 'Error', + $output, + 'Turning airplane mode on should not produce errors.' + ); + } + + public function test_airplane_mode_off(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'airplane-mode off', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Airplane mode plugin removed', + $output, + 'Turning airplane mode off should confirm the plugin was removed.' + ); + $this->assertStringNotContainsString( + 'Error', + $output, + 'Turning airplane mode off should not produce errors.' + ); + } + + public function test_airplane_mode_help_shows_usage(): void { + $output = $this->slicExec( 'airplane-mode help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'Running airplane-mode help should display usage information.' + ); + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'Running airplane-mode help should display a summary.' + ); + } +} diff --git a/tests/Cli/BaseTestCase.php b/tests/Cli/BaseTestCase.php index 1d0b145..b74139a 100644 --- a/tests/Cli/BaseTestCase.php +++ b/tests/Cli/BaseTestCase.php @@ -16,15 +16,27 @@ abstract class BaseTestCase extends TestCase { private array $createdStackIds = []; private static string $dockerMockBin = ''; + private static string $gitMockDir = ''; + private string $slicCacheDir = ''; + + /** + * Temporary directories created during the test, removed in tearDown. + * + * @var string[] + */ + private array $tempDirs = []; public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); self::$dockerMockBin = dirname( __DIR__ ) . '/_support/bin/docker-mock'; + self::$gitMockDir = dirname( __DIR__ ) . '/_support/bin/git-mock-dir'; } public function setUp(): void { parent::setUp(); $this->initialDir = getcwd(); + $this->slicCacheDir = sys_get_temp_dir() . '/slic-test-cache-' . uniqid( '', true ); + mkdir( $this->slicCacheDir . '/completions', 0777, true ); } public function tearDown(): void { @@ -39,19 +51,46 @@ public function tearDown(): void { ); } + // Remove the temporary cache directory. + if ( $this->slicCacheDir !== '' && is_dir( $this->slicCacheDir ) ) { + $this->removeDirectory( $this->slicCacheDir ); + } + + // Remove any temporary directories created during the test. + foreach ( $this->tempDirs as $dir ) { + if ( is_dir( $dir ) ) { + $this->removeDirectory( $dir ); + } + } + parent::tearDown(); } /** * Execute a slic command and return the output. * + * When `$stdin` is provided, `proc_open()` is used to pipe the content to the process's stdin + * and `SLIC_INTERACTIVE` is set to `'1'`. This path is intended for small payloads only; for + * commands that produce more than ~64 KB of combined output, the sequential read of stdout then + * stderr could deadlock. + * * @param string $command The command to execute, escaped if required. * @param array $env Optional environment variables to set for the command. + * @param string|null $stdin Optional stdin content to pipe to the process. * - * @return string The command output. + * @return string The command output (stdout and stderr merged). */ - protected function slicExec( string $command, array $env = [] ): string { + protected function slicExec( string $command, array $env = [], ?string $stdin = null ): string { $env['NO_COLOR'] = '1'; + $env['SLIC_INTERACTIVE'] = $stdin !== null ? '1' : '0'; + // Provide a dummy SSH_AUTH_SOCK to prevent setup_id() from exiting in CI. + if ( ! isset( $env['SSH_AUTH_SOCK'] ) && empty( getenv( 'SSH_AUTH_SOCK' ) ) ) { + $env['SSH_AUTH_SOCK'] = '/tmp/fake-ssh-agent.sock'; + } + // Use a temporary cache directory to avoid polluting the real cache. + if ( ! isset( $env['SLIC_CACHE_DIR'] ) && $this->slicCacheDir !== '' ) { + $env['SLIC_CACHE_DIR'] = $this->slicCacheDir; + } $envString = ''; foreach ( $env as $key => $value ) { @@ -60,8 +99,33 @@ protected function slicExec( string $command, array $env = [] ): string { $commandString = $envString . 'php ' . escapeshellarg( dirname( __DIR__, 2 ) . '/slic.php' ) . ' ' . $command; - // Redirect stderr to stdout to capture all output. - return (string) shell_exec( $commandString . ' 2>&1' ); + if ( $stdin !== null ) { + $descriptors = [ + 0 => [ 'pipe', 'r' ], + 1 => [ 'pipe', 'w' ], + ]; + // Redirect stderr to stdout so output ordering matches the shell_exec path. + $process = proc_open( $commandString . ' 2>&1', $descriptors, $pipes ); + + if ( ! is_resource( $process ) ) { + $this->fail( "proc_open() failed for command: $commandString" ); + } + + $written = fwrite( $pipes[0], $stdin ); + if ( $written === false || $written < strlen( $stdin ) ) { + fclose( $pipes[0] ); + $this->fail( "fwrite() to stdin pipe failed or wrote only $written of " . strlen( $stdin ) . " bytes for command: $commandString" ); + } + fclose( $pipes[0] ); + $output = stream_get_contents( $pipes[1] ); + fclose( $pipes[1] ); + proc_close( $process ); + + return (string) $output; + } + + // Close stdin to prevent interactive prompts from blocking, and redirect stderr to stdout. + return (string) shell_exec( $commandString . ' &1' ); } /** @@ -76,6 +140,19 @@ protected function dockerMockEnv(): array { ]; } + /** + * Returns env vars that replace git with a mock that always fails. + * + * Prevents real git clone calls from hanging on SSH authentication prompts. + * + * @return array + */ + protected function gitMockEnv(): array { + return [ + 'PATH' => self::$gitMockDir . ':' . getenv( 'PATH' ), + ]; + } + /** * Creates a temporary plugins directory with a plugin, chdirs into it, and runs `slic here`. * @@ -97,4 +174,30 @@ protected function setUpPluginsDir( string $pluginName = 'test-plugin' ): string return $pluginsDir; } + + /** + * Creates a temporary directory that is automatically cleaned up in tearDown. + * + * @return string The absolute path to the created directory. + */ + protected function createTempDir(): string { + $dir = sys_get_temp_dir() . '/slic-test-' . uniqid( '', true ); + mkdir( $dir, 0777, true ); + $this->tempDirs[] = $dir; + + return $dir; + } + + private function removeDirectory( string $dir ): void { + $items = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $items as $item ) { + $item->isDir() ? rmdir( $item->getPathname() ) : unlink( $item->getPathname() ); + } + + rmdir( $dir ); + } } diff --git a/tests/Cli/BuildPromptTest.php b/tests/Cli/BuildPromptTest.php new file mode 100644 index 0000000..6a5bf27 --- /dev/null +++ b/tests/Cli/BuildPromptTest.php @@ -0,0 +1,66 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'build-prompt on' ); + + $this->assertStringContainsString( + 'Build Prompt status: on', + $output, + 'Turning build-prompt on should confirm activation.' + ); + } + + public function test_build_prompt_off(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'build-prompt off' ); + + $this->assertStringContainsString( + 'Build Prompt status: off', + $output, + 'Turning build-prompt off should confirm deactivation.' + ); + } + + public function test_build_prompt_status(): void { + $this->setUpPluginsDir(); + + // Set to on, then check status. + $this->slicExec( 'build-prompt on' ); + $output = $this->slicExec( 'build-prompt status' ); + + $this->assertStringContainsString( + 'Interactive status is: on', + $output, + 'Status should report on after enabling build-prompt.' + ); + + // Set to off, then check status. + $this->slicExec( 'build-prompt off' ); + $output = $this->slicExec( 'build-prompt status' ); + + $this->assertStringContainsString( + 'Interactive status is: off', + $output, + 'Status should report off after disabling build-prompt.' + ); + } + + public function test_build_prompt_default_state(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'build-prompt status' ); + + $this->assertStringContainsString( + 'Interactive status is: off', + $output, + 'The default build-prompt state should be off.' + ); + } +} diff --git a/tests/Cli/BuildStackTest.php b/tests/Cli/BuildStackTest.php new file mode 100644 index 0000000..fa78dc7 --- /dev/null +++ b/tests/Cli/BuildStackTest.php @@ -0,0 +1,30 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'build-stack', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'The build-stack command should not produce error output.' + ); + } + + public function test_build_stack_specific_service(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'build-stack wordpress', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'The build-stack command for a specific service should not produce error output.' + ); + } +} diff --git a/tests/Cli/BuildSubdirTest.php b/tests/Cli/BuildSubdirTest.php new file mode 100644 index 0000000..e9874fe --- /dev/null +++ b/tests/Cli/BuildSubdirTest.php @@ -0,0 +1,70 @@ +setUpPluginsDir(); + + $this->slicExec( 'build-subdir off' ); + + $output = $this->slicExec( 'build-subdir on' ); + + $this->assertStringContainsString( + 'Build Sub-directories status: on', + $output, + 'Turning build-subdir on should confirm the on status.' + ); + } + + public function test_build_subdir_off(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'build-subdir on' ); + + $output = $this->slicExec( 'build-subdir off' ); + + $this->assertStringContainsString( + 'Build Sub-directories status: off', + $output, + 'Turning build-subdir off should confirm the off status.' + ); + } + + public function test_build_subdir_status(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'build-subdir on' ); + + $output = $this->slicExec( 'build-subdir status' ); + + $this->assertStringContainsString( + 'Sub-directories build status is: on', + $output, + 'The status subcommand should report the current on state.' + ); + + $this->slicExec( 'build-subdir off' ); + + $output = $this->slicExec( 'build-subdir status' ); + + $this->assertStringContainsString( + 'Sub-directories build status is: off', + $output, + 'The status subcommand should report the current off state.' + ); + } + + public function test_build_subdir_default_state(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'build-subdir status' ); + + $this->assertMatchesRegularExpression( + '/Sub-directories build status is: (on|off)/', + $output, + 'The default state should be reported as on or off.' + ); + } +} diff --git a/tests/Cli/CacheTest.php b/tests/Cli/CacheTest.php new file mode 100644 index 0000000..cea9a39 --- /dev/null +++ b/tests/Cli/CacheTest.php @@ -0,0 +1,103 @@ +setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'cache on', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Unknown command', + $output, + 'The cache on command should be recognized.' + ); + $this->assertStringNotContainsString( + 'Failed', + $output, + 'The cache on command should not report failure.' + ); + } + + public function test_cache_off(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'cache off', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Unknown command', + $output, + 'The cache off command should be recognized.' + ); + $this->assertStringNotContainsString( + 'Failed', + $output, + 'The cache off command should not report failure.' + ); + } + + public function test_cache_status(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'cache status', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Unknown command', + $output, + 'The cache status command should be recognized.' + ); + $this->assertStringNotContainsString( + 'Failed', + $output, + 'The cache status command should not report failure.' + ); + } + + public function test_cache_default_is_status(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'cache', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Unknown command', + $output, + 'The cache command with no argument should be recognized.' + ); + $this->assertStringNotContainsString( + 'Failed', + $output, + 'The cache command with no argument should not report failure.' + ); + } + + public function test_cache_help_shows_usage(): void { + $output = $this->slicExec( 'help cache' ); + + $this->assertStringContainsString( + 'cache', + $output, + 'The cache help should reference the cache command.' + ); + $this->assertStringContainsString( + 'status', + $output, + 'The cache help should list the status subcommand.' + ); + $this->assertStringContainsString( + 'on', + $output, + 'The cache help should list the on subcommand.' + ); + $this->assertStringContainsString( + 'off', + $output, + 'The cache help should list the off subcommand.' + ); + } +} diff --git a/tests/Cli/CcTest.php b/tests/Cli/CcTest.php new file mode 100644 index 0000000..9ae3c72 --- /dev/null +++ b/tests/Cli/CcTest.php @@ -0,0 +1,29 @@ +slicExec( 'cc' ); + + $this->assertStringContainsString( + 'This command requires a target set using the use command.', + $output, + 'Running cc without a use target should show an error.' + ); + } + + public function test_cc_passes_commands_to_container(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'cc generate:wpunit wpunit Foo', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'The cc command should report the use target.' + ); + } +} diff --git a/tests/Cli/CompletionTest.php b/tests/Cli/CompletionTest.php new file mode 100644 index 0000000..1f91673 --- /dev/null +++ b/tests/Cli/CompletionTest.php @@ -0,0 +1,160 @@ +slicExec( 'completion show bash' ); + + $this->assertStringContainsString( + 'Completion script for bash:', + $output, + 'The output should indicate the bash completion script.' + ); + + $this->assertStringContainsString( + '#!/usr/bin/env bash', + $output, + 'The output should contain a bash shebang.' + ); + + $this->assertStringContainsString( + '_slic_completions', + $output, + 'The output should contain the bash completion function.' + ); + } + + public function test_completion_show_zsh(): void { + $output = $this->slicExec( 'completion show zsh' ); + + $this->assertStringContainsString( + 'Completion script for zsh:', + $output, + 'The output should indicate the zsh completion script.' + ); + + $this->assertStringContainsString( + '#compdef slic', + $output, + 'The output should contain the zsh compdef directive.' + ); + + $this->assertStringContainsString( + '_slic', + $output, + 'The output should contain the zsh completion function.' + ); + } + + public function test_completion_cache_clear(): void { + $output = $this->slicExec( 'completion cache-clear' ); + + $this->assertStringContainsString( + 'Clearing completion cache', + $output, + 'The output should mention clearing the cache.' + ); + + $this->assertMatchesRegularExpression( + '/Cleared \d+ cached completion file/', + $output, + 'The output should confirm how many cached files were cleared.' + ); + } + + public function test_completion_no_args_shows_instructions(): void { + // Use a clean HOME so is_installed() returns false, ensuring install instructions are shown. + $tempHome = $this->createTempDir(); + + $output = $this->slicExec( 'completion', [ 'HOME' => $tempHome ] ); + + $this->assertStringContainsString( + 'Detected shell:', + $output, + 'The output should show the detected shell.' + ); + + $this->assertStringContainsString( + 'completion install', + $output, + 'The output should mention the completion install subcommand.' + ); + } + + public function test_completion_install_confirm_writes_config(): void { + $tempHome = $this->createTempDir(); + file_put_contents( $tempHome . '/.bashrc', '' ); + + $output = $this->slicExec( + 'completion install bash', + [ 'HOME' => $tempHome ], + "yes\n" + ); + + $bashrc = file_get_contents( $tempHome . '/.bashrc' ); + + $this->assertStringContainsString( + 'Continue with installation?', + $output, + 'The install flow should prompt for confirmation.' + ); + $this->assertStringContainsString( + 'completions installed successfully', + $output, + 'The output should confirm successful installation.' + ); + $this->assertStringContainsString( + 'slic completions', + $bashrc, + 'The .bashrc file should contain the slic completions block.' + ); + } + + public function test_completion_install_decline_cancels(): void { + $tempHome = $this->createTempDir(); + file_put_contents( $tempHome . '/.bashrc', '' ); + + $output = $this->slicExec( + 'completion install bash', + [ 'HOME' => $tempHome ], + "no\n" + ); + + $bashrc = file_get_contents( $tempHome . '/.bashrc' ); + + $this->assertStringContainsString( + 'Installation cancelled.', + $output, + 'Declining the prompt should cancel installation.' + ); + $this->assertEmpty( + $bashrc, + 'The .bashrc file should remain untouched after cancellation.' + ); + } + + public function test_completion_install_fish_confirm_creates_symlink(): void { + $tempHome = $this->createTempDir(); + mkdir( $tempHome . '/.config/fish/completions', 0777, true ); + + $output = $this->slicExec( + 'completion install fish', + [ 'HOME' => $tempHome ], + "yes\n" + ); + + $symlinkPath = $tempHome . '/.config/fish/completions/slic.fish'; + + $this->assertStringContainsString( + 'completions installed successfully', + $output, + 'The output should confirm successful fish installation.' + ); + $this->assertTrue( + is_link( $symlinkPath ), + 'A symlink should be created for the fish completion script.' + ); + } +} diff --git a/tests/Cli/ComposerCacheTest.php b/tests/Cli/ComposerCacheTest.php new file mode 100644 index 0000000..768195c --- /dev/null +++ b/tests/Cli/ComposerCacheTest.php @@ -0,0 +1,69 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'composer-cache' ); + + $this->assertStringContainsString( + 'Composer cache directory:', + $output, + 'The composer-cache command should show the current cache directory setting.' + ); + } + + public function test_composer_cache_set(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'composer-cache set /tmp/test-cache', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Composer cache directory:', + $output, + 'The set subcommand should display the cache directory label.' + ); + + $this->assertStringContainsString( + '/tmp/test-cache', + $output, + 'The set subcommand should confirm the new cache directory path.' + ); + } + + public function test_composer_cache_unset(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'composer-cache unset', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Composer cache directory:', + $output, + 'The unset subcommand should display the cache directory label.' + ); + } + + public function test_composer_cache_set_nonexistent_path(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( + 'composer-cache set /tmp/nonexistent-path-' . uniqid(), + $this->dockerMockEnv() + ); + + $this->assertStringContainsString( + 'Composer cache directory:', + $output, + 'Setting a non-existent path should still be accepted.' + ); + + $this->assertStringNotContainsString( + 'error', + strtolower( $output ), + 'No error should be reported for a non-existent cache path.' + ); + } +} diff --git a/tests/Cli/ComposerTest.php b/tests/Cli/ComposerTest.php new file mode 100644 index 0000000..d2deb5a --- /dev/null +++ b/tests/Cli/ComposerTest.php @@ -0,0 +1,91 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'composer install' ); + + $this->assertStringContainsString( + 'This command requires a target set using the', + $output, + 'Running composer without a use target should show an error.' + ); + } + + public function test_composer_get_version(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'composer get-version', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'The get-version output should show the current use target.' + ); + } + + public function test_composer_set_version_1(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'composer set-version 1', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Composer version set to 1', + $output, + 'Setting version to 1 should confirm the change.' + ); + } + + public function test_composer_set_version_2(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'composer set-version 2', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Composer version set to 2', + $output, + 'Setting version to 2 should confirm the change.' + ); + } + + public function test_composer_reset_version(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + // Set to 1 first so reset has something to change. + $this->slicExec( 'composer set-version 1', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'composer reset-version', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Composer version reset to default', + $output, + 'Resetting the version should confirm the reset to default.' + ); + } + + public function test_composer_passes_commands_to_container(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'composer install', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'Running composer install with docker mock should pass through without error.' + ); + } +} diff --git a/tests/Cli/ConfigTest.php b/tests/Cli/ConfigTest.php new file mode 100644 index 0000000..88cb194 --- /dev/null +++ b/tests/Cli/ConfigTest.php @@ -0,0 +1,83 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'config' ); + + $this->assertStringContainsString( + 'services:', + $output, + 'The config output should contain a services section.' + ); + $this->assertStringContainsString( + 'image:', + $output, + 'The config output should contain image definitions.' + ); + $this->assertStringContainsString( + 'networks:', + $output, + 'The config output should contain a networks section.' + ); + } + + public function test_config_reflects_php_version(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'php-version set 8.1 --skip-rebuild' ); + + $output = $this->slicExec( 'config' ); + + $this->assertStringContainsString( + 'slic-php8.1', + $output, + 'The config output should reference PHP 8.1 in the image name.' + ); + $this->assertStringContainsString( + 'slic-wordpress-php8.1', + $output, + 'The config output should reference PHP 8.1 in the WordPress image name.' + ); + } + + public function test_config_reflects_plugins_directory(): void { + $pluginsDir = $this->setUpPluginsDir(); + + $output = $this->slicExec( 'config' ); + + $this->assertStringContainsString( + realpath( $pluginsDir ), + $output, + 'The config output should contain the plugins directory path.' + ); + } + + public function test_config_reflects_xdebug_settings(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'xdebug on' ); + + $output = $this->slicExec( 'config' ); + + $this->assertStringContainsString( + 'XDEBUG_DISABLE: "0"', + $output, + 'The config should show XDEBUG_DISABLE as 0 when xdebug is on.' + ); + + $this->slicExec( 'xdebug off' ); + + $output = $this->slicExec( 'config' ); + + $this->assertStringContainsString( + 'XDEBUG_DISABLE: "1"', + $output, + 'The config should show XDEBUG_DISABLE as 1 when xdebug is off.' + ); + } +} diff --git a/tests/Cli/DcExecPsTest.php b/tests/Cli/DcExecPsTest.php new file mode 100644 index 0000000..fe68807 --- /dev/null +++ b/tests/Cli/DcExecPsTest.php @@ -0,0 +1,92 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'dc ps', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'slic', + $output, + 'The dc ps command should run without error.' + ); + } + + public function test_dc_help_shows_usage(): void { + $output = $this->slicExec( 'dc help' ); + + $this->assertStringContainsString( + 'Runs a docker compose command in the stack', + $output, + 'The dc help command should show the summary.' + ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The dc help command should show usage information.' + ); + } + + public function test_exec_passes_command(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'exec "whoami"', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'The exec command should run without error.' + ); + + $this->assertStringNotContainsString( + 'Please specify a bash command', + $output, + 'The exec command should not complain about missing arguments.' + ); + } + + public function test_exec_help_shows_usage(): void { + $output = $this->slicExec( 'exec help' ); + + $this->assertStringContainsString( + 'Runs a bash command in the stack', + $output, + 'The exec help command should show the summary.' + ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The exec help command should show usage information.' + ); + } + + public function test_ps_lists_containers(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'ps', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'slic', + $output, + 'The ps command should run without error.' + ); + } + + public function test_ps_help_shows_usage(): void { + $output = $this->slicExec( 'ps help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The ps help command should show usage information.' + ); + } +} diff --git a/tests/Cli/DebugTest.php b/tests/Cli/DebugTest.php new file mode 100644 index 0000000..356f556 --- /dev/null +++ b/tests/Cli/DebugTest.php @@ -0,0 +1,69 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'debug on' ); + + $this->assertStringContainsString( + 'Debug status: on', + $output, + 'Turning debug on should confirm activation.' + ); + } + + public function test_debug_off(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'debug off' ); + + $this->assertStringContainsString( + 'Debug status: off', + $output, + 'Turning debug off should confirm deactivation.' + ); + } + + public function test_debug_status(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'debug status' ); + + $this->assertStringContainsString( + 'Debug status is:', + $output, + 'The status subcommand should report the current debug state.' + ); + } + + public function test_debug_no_args_defaults_to_status(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'debug' ); + + $this->assertStringContainsString( + 'Debug status is:', + $output, + 'Running debug with no argument should default to showing status.' + ); + } + + public function test_debug_persists_across_commands(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'debug on' ); + + // The debug command writes CLI_VERBOSITY to the run settings file; verify it persists via info. + $output = $this->slicExec( 'info' ); + + $this->assertMatchesRegularExpression( + '/CLI_VERBOSITY\s*[:=]\s*1/', + $output, + 'CLI_VERBOSITY should remain 1 after setting debug on in a previous command.' + ); + } +} diff --git a/tests/Cli/GroupTest.php b/tests/Cli/GroupTest.php new file mode 100644 index 0000000..4266af8 --- /dev/null +++ b/tests/Cli/GroupTest.php @@ -0,0 +1,16 @@ +slicExec( 'group help' ); + + $this->assertStringContainsString( + 'Unknown command', + $output, + 'The group command should not be recognized.' + ); + } +} diff --git a/tests/Cli/HelpTest.php b/tests/Cli/HelpTest.php new file mode 100644 index 0000000..fbccfa4 --- /dev/null +++ b/tests/Cli/HelpTest.php @@ -0,0 +1,74 @@ +slicExec( 'help' ); + + $this->assertStringContainsString( + 'slic version', + $output, + 'The help output should contain the version string.' + ); + } + + public function test_help_output_lists_popular_commands(): void { + $output = $this->slicExec( 'help' ); + + $this->assertStringContainsString( + 'Popular:', + $output, + 'The help output should contain a Popular section.' + ); + + $popularCommands = [ 'composer', 'run', 'use', 'help' ]; + foreach ( $popularCommands as $command ) { + $this->assertMatchesRegularExpression( + '/Popular:.*^\s+' . preg_quote( $command, '/' ) . '\s/ms', + $output, + "The Popular section should list the '{$command}' command." + ); + } + } + + public function test_help_output_lists_advanced_commands(): void { + $output = $this->slicExec( 'help' ); + + $this->assertStringContainsString( + 'Advanced:', + $output, + 'The help output should contain an Advanced section.' + ); + + $advancedCommands = [ 'cache', 'debug', 'dc' ]; + foreach ( $advancedCommands as $command ) { + $this->assertMatchesRegularExpression( + '/Advanced:.*^\s+' . preg_quote( $command, '/' ) . '\s/ms', + $output, + "The Advanced section should list the '{$command}' command." + ); + } + } + + public function test_help_output_includes_usage_hint(): void { + $output = $this->slicExec( 'help' ); + + $this->assertStringContainsString( + 'Type slic help', + $output, + 'The help output should contain guidance on getting per-command help.' + ); + } + + public function test_unknown_command_shows_error(): void { + $output = $this->slicExec( 'nonexistent-cmd-xyz' ); + + $this->assertStringContainsString( + 'Unknown command: nonexistent-cmd-xyz', + $output, + 'Running an unrecognized command should display an error message.' + ); + } +} diff --git a/tests/Cli/HereTest.php b/tests/Cli/HereTest.php new file mode 100644 index 0000000..58a0129 --- /dev/null +++ b/tests/Cli/HereTest.php @@ -0,0 +1,87 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'stack list' ); + + $this->assertStringContainsString( + realpath( $pluginsDir ), + $output, + 'The stack list should contain the plugins directory.' + ); + } + + public function test_here_output_confirms_directory(): void { + $pluginsDir = $this->setUpPluginsDir(); + + chdir( $pluginsDir ); + $output = $this->slicExec( 'here' ); + + $this->assertStringContainsString( + realpath( $pluginsDir ), + $output, + 'The here output should contain the set directory path.' + ); + } + + public function test_here_reset_unsets_directory(): void { + $pluginsDir = $this->setUpPluginsDir(); + + $resetOutput = $this->slicExec( 'here reset' ); + + // The reset output should reference the default _plugins directory as the new stack ID. + $this->assertStringContainsString( + '_plugins', + $resetOutput, + 'The here reset output should reference the default _plugins directory.' + ); + + $this->assertStringContainsString( + 'Stack', + $resetOutput, + 'The here reset output should confirm a stack operation.' + ); + } + + public function test_here_from_subdirectory(): void { + $pluginsDir = $this->setUpPluginsDir(); + + chdir( $pluginsDir . '/test-plugin' ); + $output = $this->slicExec( 'here' ); + + $this->assertStringContainsString( + realpath( $pluginsDir . '/test-plugin' ), + $output, + 'Running here from a plugin subdirectory should set that subdirectory as the stack ID.' + ); + } + + public function test_here_in_non_plugin_directory_still_creates_stack(): void { + // First set up a valid plugins dir so we have a registered stack context. + $this->setUpPluginsDir(); + + $emptyDir = Directory::createTemp()->getAbsolutePath(); + chdir( $emptyDir ); + + $output = $this->slicExec( 'here' ); + + $this->assertStringContainsString( + 'Stack created successfully', + $output, + 'Running here in a directory without plugins should still create a stack.' + ); + + $this->assertStringContainsString( + realpath( $emptyDir ), + $output, + 'The stack ID should reference the empty directory.' + ); + } +} diff --git a/tests/Cli/HostIpTest.php b/tests/Cli/HostIpTest.php new file mode 100644 index 0000000..037e192 --- /dev/null +++ b/tests/Cli/HostIpTest.php @@ -0,0 +1,37 @@ + + */ + private function hostIpMockEnv(): array { + return [ + 'SLIC_DOCKER_BIN' => dirname( __DIR__ ) . '/_support/bin/docker-mock-host-ip', + 'SLIC_DOCKER_COMPOSE_BIN' => dirname( __DIR__ ) . '/_support/bin/docker-mock', + ]; + } + + public function test_host_ip_returns_ip_address(): void { + $output = $this->slicExec( 'host-ip', $this->hostIpMockEnv() ); + + $this->assertMatchesRegularExpression( + '/\d+\.\d+\.\d+\.\d+/', + $output, + 'The host-ip command should return an IP address.' + ); + } + + public function test_host_ip_returns_nonblank(): void { + $output = $this->slicExec( 'host-ip', $this->hostIpMockEnv() ); + + $this->assertNotEmpty( + trim( $output ), + 'The host-ip command should return non-empty output.' + ); + } +} diff --git a/tests/Cli/InfoTest.php b/tests/Cli/InfoTest.php new file mode 100644 index 0000000..60ebd67 --- /dev/null +++ b/tests/Cli/InfoTest.php @@ -0,0 +1,72 @@ +slicExec( 'info' ); + + $this->assertMatchesRegularExpression( + '/slic version \d+\.\d+\.\d+/', + $output, + 'The info output should include the slic version in semver format.' + ); + } + + public function test_info_shows_plugins_directory(): void { + $pluginsDir = $this->setUpPluginsDir(); + + $output = $this->slicExec( 'info' ); + + $this->assertStringContainsString( + 'SLIC_PLUGINS_DIR', + $output, + 'The info output should include the SLIC_PLUGINS_DIR key.' + ); + $this->assertStringContainsString( + $pluginsDir, + $output, + 'The info output should include the plugins directory path.' + ); + } + + public function test_info_shows_current_target(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'info' ); + + $this->assertMatchesRegularExpression( + '/SLIC_CURRENT_PROJECT:\s*test-plugin/', + $output, + 'The info output should show test-plugin as the SLIC_CURRENT_PROJECT value.' + ); + } + + public function test_info_shows_php_version(): void { + $output = $this->slicExec( 'info' ); + + $this->assertMatchesRegularExpression( + '/SLIC_PHP_VERSION:\s*7\.4/', + $output, + 'The info output should show 7.4 as the default SLIC_PHP_VERSION value.' + ); + } + + public function test_info_without_stack_shows_defaults(): void { + $output = $this->slicExec( 'info' ); + + $this->assertStringContainsString( + 'Current configuration:', + $output, + 'The info output should include the current configuration header.' + ); + $this->assertMatchesRegularExpression( + '/SLIC_CURRENT_PROJECT:\s*$\n/m', + $output, + 'The default SLIC_CURRENT_PROJECT should be empty when no target is set.' + ); + } +} diff --git a/tests/Cli/InitTest.php b/tests/Cli/InitTest.php new file mode 100644 index 0000000..4349f99 --- /dev/null +++ b/tests/Cli/InitTest.php @@ -0,0 +1,56 @@ +slicExec( 'init', $this->gitMockEnv() ); + + $this->assertStringContainsString( + 'Using', + $output, + 'Running init without a plugin name should show the current (empty) target.' + ); + + $this->assertStringContainsString( + 'Finished initializing', + $output, + 'Running init without a plugin should still finish initializing.' + ); + } + + public function test_init_nonexistent_plugin_shows_error(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'init nonexistent', $this->gitMockEnv() ); + + $this->assertStringContainsString( + 'Cloning nonexistent', + $output, + 'Running init with a nonexistent plugin should attempt to clone it.' + ); + + $this->assertStringContainsString( + 'Could not clone', + $output, + 'Running init with a nonexistent plugin should show a clone error.' + ); + } + + public function test_init_help_shows_usage(): void { + $output = $this->slicExec( 'init help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The init help output should contain a USAGE section.' + ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The init help output should contain a SUMMARY section.' + ); + } +} diff --git a/tests/Cli/InteractiveTest.php b/tests/Cli/InteractiveTest.php new file mode 100644 index 0000000..c91b8cb --- /dev/null +++ b/tests/Cli/InteractiveTest.php @@ -0,0 +1,73 @@ +setUpPluginsDir(); + + $this->slicExec( 'interactive off' ); + + $output = $this->slicExec( 'interactive on' ); + + $this->assertStringContainsString( + 'Interactive status: on', + $output, + 'Turning interactive on should confirm activation.' + ); + } + + public function test_interactive_off(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'interactive on' ); + + $output = $this->slicExec( 'interactive off' ); + + $this->assertStringContainsString( + 'Interactive status: off', + $output, + 'Turning interactive off should confirm deactivation.' + ); + } + + public function test_interactive_status(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'interactive on' ); + + $output = $this->slicExec( 'interactive status' ); + + $this->assertStringContainsString( + 'Interactive status is: on', + $output, + 'The status subcommand should report the current interactive state.' + ); + + $this->slicExec( 'interactive off' ); + + $output = $this->slicExec( 'interactive status' ); + + $this->assertStringContainsString( + 'Interactive status is: off', + $output, + 'The status subcommand should reflect the updated state after turning off.' + ); + } + + public function test_interactive_default_state(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'interactive off' ); + + // Calling interactive with no argument should default to "on". + $output = $this->slicExec( 'interactive' ); + + $this->assertStringContainsString( + 'Interactive status: on', + $output, + 'Calling interactive with no argument should default to turning it on.' + ); + } +} diff --git a/tests/Cli/LogsTest.php b/tests/Cli/LogsTest.php new file mode 100644 index 0000000..5b66828 --- /dev/null +++ b/tests/Cli/LogsTest.php @@ -0,0 +1,33 @@ +slicExec( 'logs help' ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The logs help should show the summary section.' + ); + $this->assertStringContainsString( + 'USAGE', + $output, + 'The logs help should show the usage section.' + ); + } + + public function test_logs_runs_without_error(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'logs', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'The logs command should not produce error output.' + ); + } +} diff --git a/tests/Cli/MysqlTest.php b/tests/Cli/MysqlTest.php new file mode 100644 index 0000000..97bffbc --- /dev/null +++ b/tests/Cli/MysqlTest.php @@ -0,0 +1,38 @@ +slicExec( 'mysql help' ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The mysql help should show the summary section.' + ); + $this->assertStringContainsString( + 'USAGE', + $output, + 'The mysql help should show the usage section.' + ); + } + + public function test_mysql_runs_without_error(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'mysql', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'The mysql command should not produce error output.' + ); + $this->assertStringNotContainsString( + 'not a valid command', + $output, + 'The mysql command should be recognized as a valid command.' + ); + } +} diff --git a/tests/Cli/NpmTest.php b/tests/Cli/NpmTest.php new file mode 100644 index 0000000..a5dfc65 --- /dev/null +++ b/tests/Cli/NpmTest.php @@ -0,0 +1,38 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'npm install' ); + + $this->assertStringContainsString( + 'This command requires a target set using the', + $output, + 'Running npm without a use target should show an error.' + ); + } + + public function test_npm_passes_commands_to_container(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'npm install', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'Running npm install with docker mock should show the current use target.' + ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'Running npm install with docker mock should not produce an error.' + ); + } +} diff --git a/tests/Cli/PhpcsPhpcbfTest.php b/tests/Cli/PhpcsPhpcbfTest.php new file mode 100644 index 0000000..f1b3828 --- /dev/null +++ b/tests/Cli/PhpcsPhpcbfTest.php @@ -0,0 +1,58 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'phpcs' ); + + $this->assertStringContainsString( + 'This command requires a target set using the use command.', + $output, + 'Running phpcs without a use target should show an error.' + ); + } + + public function test_phpcs_with_target_runs(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'phpcs', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'Running phpcs with a use target should show the target name.' + ); + } + + public function test_phpcbf_requires_use_target(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'phpcbf' ); + + $this->assertStringContainsString( + 'This command requires a target set using the use command.', + $output, + 'Running phpcbf without a use target should show an error.' + ); + } + + public function test_phpcbf_with_target_runs(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'phpcbf', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'Running phpcbf with a use target should show the target name.' + ); + } +} diff --git a/tests/Cli/PlaywrightTest.php b/tests/Cli/PlaywrightTest.php new file mode 100644 index 0000000..e39dea5 --- /dev/null +++ b/tests/Cli/PlaywrightTest.php @@ -0,0 +1,37 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'playwright test' ); + + $this->assertStringContainsString( + 'This command requires a target set using the use command.', + $output, + 'Running playwright without a use target should show an error.' + ); + } + + public function test_playwright_passes_commands_to_container(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'playwright test', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'Running playwright test with docker mock should show the current use target.' + ); + $this->assertStringNotContainsString( + 'Error', + $output, + 'Running playwright test with docker mock should not produce errors.' + ); + } +} diff --git a/tests/Cli/ResetTest.php b/tests/Cli/ResetTest.php new file mode 100644 index 0000000..78d1367 --- /dev/null +++ b/tests/Cli/ResetTest.php @@ -0,0 +1,96 @@ +setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $usingBefore = $this->slicExec( 'using' ); + $this->assertStringContainsString( + 'Using test-plugin', + $usingBefore, + 'The target should be set before reset.' + ); + + $this->slicExec( 'reset', $this->dockerMockEnv() ); + $this->slicExec( 'here' ); + + $usingAfter = $this->slicExec( 'using' ); + $this->assertStringContainsString( + 'Currently not using any target', + $usingAfter, + 'The target should be cleared after reset and re-initialization.' + ); + } + + public function test_reset_restores_default_php_version(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'php-version set 8.1 --skip-rebuild' ); + + $phpVersionBefore = $this->slicExec( 'php-version' ); + $this->assertStringContainsString( + '8.1', + $phpVersionBefore, + 'The PHP version should be staged to 8.1 before reset.' + ); + + // Reset preserves per-stack state; an explicit php-version reset is needed. + $this->slicExec( 'reset', $this->dockerMockEnv() ); + $this->slicExec( 'php-version reset --skip-rebuild' ); + + $phpVersionAfter = $this->slicExec( 'php-version' ); + $this->assertStringContainsString( + '7.4', + $phpVersionAfter, + 'The PHP version should be restored to 7.4 after reset.' + ); + } + + public function test_reset_output_confirms(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'reset', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Removing', + $output, + 'The reset output should mention removing files.' + ); + $this->assertStringContainsString( + '.env.slic.run', + $output, + 'The reset output should mention the run settings file.' + ); + $this->assertStringContainsString( + 'done', + $output, + 'The reset output should confirm completion.' + ); + } + + public function test_reset_clears_xdebug_settings(): void { + $this->setUpPluginsDir(); + + $setOutput = $this->slicExec( 'xdebug port 9009' ); + $this->assertStringContainsString( + 'XDP=9009', + $setOutput, + 'The xdebug port command should confirm setting XDP=9009.' + ); + + $this->slicExec( 'reset', $this->dockerMockEnv() ); + $this->slicExec( 'here' ); + + $xdebugAfter = $this->slicExec( 'xdebug status' ); + $this->assertStringNotContainsString( + '9009', + $xdebugAfter, + 'The xdebug port should no longer be 9009 after reset and re-initialization.' + ); + } +} diff --git a/tests/Cli/RunTest.php b/tests/Cli/RunTest.php new file mode 100644 index 0000000..1b588bc --- /dev/null +++ b/tests/Cli/RunTest.php @@ -0,0 +1,36 @@ +slicExec( 'run' ); + + $this->assertStringContainsString( + 'This command requires a target set using the use command.', + $output, + 'Running run without a use target should show an error.' + ); + } + + public function test_run_passes_commands_to_container(): void { + $pluginsDir = $this->setUpPluginsDir(); + + // Create a codeception.dist.yml so the run command does not bail for missing config. + file_put_contents( + $pluginsDir . '/test-plugin/codeception.dist.yml', + "paths:\n tests: tests\n" + ); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'run wpunit', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'The run command should report the current use target.' + ); + } +} diff --git a/tests/Cli/ShellTest.php b/tests/Cli/ShellTest.php new file mode 100644 index 0000000..3e86e7f --- /dev/null +++ b/tests/Cli/ShellTest.php @@ -0,0 +1,58 @@ +slicExec( 'shell' ); + + $this->assertStringContainsString( + 'This command requires a target set using the use command.', + $output, + 'Running shell without a use target should show an error.' + ); + } + + public function test_shell_with_target_runs(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'shell', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'The shell command should show the current target.' + ); + } + + public function test_ssh_is_alias_for_shell(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'ssh', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'not a valid command', + $output, + 'The ssh command should be a recognized command.' + ); + } + + public function test_shell_help_shows_usage(): void { + $output = $this->slicExec( 'shell help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The shell help should display usage information.' + ); + + $this->assertStringContainsString( + 'shell', + $output, + 'The shell help should reference the shell command.' + ); + } +} diff --git a/tests/Cli/StackTest.php b/tests/Cli/StackTest.php new file mode 100644 index 0000000..5e0b132 --- /dev/null +++ b/tests/Cli/StackTest.php @@ -0,0 +1,110 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'stack list' ); + + $this->assertStringContainsString( + $pluginsDir, + $output, + 'The stack list should include the registered plugins directory.' + ); + } + + public function test_stack_list_empty_when_no_stacks(): void { + $this->slicExec( 'stack stop all -y', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'stack list' ); + + $this->assertStringContainsString( + 'No stacks registered', + $output, + 'The stack list should indicate no stacks are registered.' + ); + } + + public function test_stack_info_shows_stack_details(): void { + $pluginsDir = $this->setUpPluginsDir(); + + $output = $this->slicExec( 'stack info' ); + + $this->assertStringContainsString( + 'Stack ID:', + $output, + 'The stack info output should contain the Stack ID label.' + ); + $this->assertStringContainsString( + $pluginsDir, + $output, + 'The stack info output should contain the plugins directory path.' + ); + $this->assertStringContainsString( + 'Status:', + $output, + 'The stack info output should contain the Status label.' + ); + } + + public function test_stack_stop_removes_stack(): void { + // Create the stack manually to avoid tearDown trying to stop it again. + $pluginsDir = Directory::createTemp() + ->createPlugin( 'test-plugin' ) + ->getAbsolutePath(); + chdir( $pluginsDir ); + $this->slicExec( 'here' ); + $stackId = realpath( $pluginsDir ); + + $this->slicExec( 'stack stop ' . escapeshellarg( $stackId ), $this->dockerMockEnv() ); + + $output = $this->slicExec( 'stack list' ); + $this->assertStringNotContainsString( + $stackId, + $output, + 'The stack list should not contain the stopped stack.' + ); + } + + public function test_stack_stop_all_stops_all_stacks(): void { + $dirA = $this->setUpPluginsDir( 'plugin-alpha' ); + $dirB = $this->setUpPluginsDir( 'plugin-beta' ); + + $this->slicExec( 'stack stop all -y', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'stack list' ); + $this->assertStringNotContainsString( + $dirA, + $output, + 'The stack list should not contain the first stack after stopping all.' + ); + $this->assertStringNotContainsString( + $dirB, + $output, + 'The stack list should not contain the second stack after stopping all.' + ); + } + + public function test_stack_list_shows_multiple_stacks(): void { + $dirA = $this->setUpPluginsDir( 'plugin-one' ); + $dirB = $this->setUpPluginsDir( 'plugin-two' ); + + $output = $this->slicExec( 'stack list' ); + + $this->assertStringContainsString( + $dirA, + $output, + 'The stack list should contain the first registered stack.' + ); + $this->assertStringContainsString( + $dirB, + $output, + 'The stack list should contain the second registered stack.' + ); + } +} diff --git a/tests/Cli/StartStopTest.php b/tests/Cli/StartStopTest.php new file mode 100644 index 0000000..d4ecedf --- /dev/null +++ b/tests/Cli/StartStopTest.php @@ -0,0 +1,126 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'start', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'All containers are running', + $output, + 'The start command should report all containers running.' + ); + $this->assertStringNotContainsString( + 'Error', + $output, + 'The start command should not produce error output.' + ); + } + + public function test_up_is_alias_for_start(): void { + $this->setUpPluginsDir(); + + $startOutput = $this->slicExec( 'start', $this->dockerMockEnv() ); + $upOutput = $this->slicExec( 'up', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'All containers are running', + $upOutput, + 'The up command should report all containers running.' + ); + $this->assertStringContainsString( + 'All containers are running', + $startOutput, + 'The start command should report all containers running.' + ); + } + + public function test_stop_runs_without_error(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'stop', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'All services have been stopped', + $output, + 'The stop command should report all services stopped.' + ); + $this->assertStringNotContainsString( + 'Error', + $output, + 'The stop command should not produce error output.' + ); + } + + public function test_down_is_alias_for_stop(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'down', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'All services have been stopped', + $output, + 'The down command should report all services stopped.' + ); + } + + public function test_restart_runs_without_error(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'restart', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Error', + $output, + 'The restart command should not produce error output.' + ); + } + + public function test_start_help_shows_usage(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'start help' ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The start help should show the summary section.' + ); + $this->assertStringContainsString( + 'USAGE', + $output, + 'The start help should show the usage section.' + ); + $this->assertStringContainsString( + 'Starts containers in the stack', + $output, + 'The start help should describe its purpose.' + ); + } + + public function test_stop_help_shows_usage(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'stop help' ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The stop help should show the summary section.' + ); + $this->assertStringContainsString( + 'USAGE', + $output, + 'The stop help should show the usage section.' + ); + $this->assertStringContainsString( + 'Stops containers in the stack', + $output, + 'The stop help should describe its purpose.' + ); + } +} diff --git a/tests/Cli/TargetTest.php b/tests/Cli/TargetTest.php new file mode 100644 index 0000000..55f2fca --- /dev/null +++ b/tests/Cli/TargetTest.php @@ -0,0 +1,62 @@ +slicExec( 'target help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The target help output should display usage information.' + ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The target help output should display a summary.' + ); + + $this->assertStringContainsString( + 'slic.php target', + $output, + 'The target help output should contain the command signature.' + ); + } + + public function test_target_interactive_flow(): void { + $this->setUpPluginsDir(); + + // Pipe: target "test-plugin", end targets, command "info", end commands, confirm. + $stdin = "test-plugin\n\ninfo\n\n\n"; + $output = $this->slicExec( 'target', $this->dockerMockEnv(), $stdin ); + + $this->assertStringContainsString( + 'Target (return when done):', + $output, + 'The interactive target flow should prompt for targets.' + ); + $this->assertStringContainsString( + 'Targets:', + $output, + 'The output should display the "Targets:" label after collecting targets.' + ); + $this->assertStringContainsString( + 'test-plugin', + $output, + 'The output should include the collected target name.' + ); + $this->assertStringContainsString( + 'Command (return when done):', + $output, + 'The interactive target flow should prompt for commands.' + ); + $this->assertStringContainsString( + 'Are you sure you want to run these commands on', + $output, + 'The interactive target flow should prompt for confirmation before executing.' + ); + } +} diff --git a/tests/Cli/UpdateDumpTest.php b/tests/Cli/UpdateDumpTest.php new file mode 100644 index 0000000..dc1cac4 --- /dev/null +++ b/tests/Cli/UpdateDumpTest.php @@ -0,0 +1,48 @@ +slicExec( 'update-dump help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The help output should contain the USAGE section.' + ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The help output should contain the SUMMARY section.' + ); + } + + public function test_update_dump_requires_file_argument(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'update-dump', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'Undefined array key 0', + $output, + 'Running update-dump without a file argument should produce a warning about the missing argument.' + ); + } + + public function test_update_dump_nonexistent_file_shows_error(): void { + $this->setUpPluginsDir(); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'update-dump nonexistent.sql', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'does not exist', + $output, + 'Running update-dump with a nonexistent file should show an error about the missing file.' + ); + } +} diff --git a/tests/Cli/UpdateTest.php b/tests/Cli/UpdateTest.php new file mode 100644 index 0000000..f16a60c --- /dev/null +++ b/tests/Cli/UpdateTest.php @@ -0,0 +1,38 @@ +slicExec( 'update help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The update help output should contain a USAGE section.' + ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The update help output should contain a SUMMARY section.' + ); + } + + public function test_upgrade_help_shows_usage(): void { + $output = $this->slicExec( 'upgrade help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The upgrade help output should contain a USAGE section.' + ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The upgrade help output should contain a SUMMARY section.' + ); + } +} diff --git a/tests/Cli/UseTest.php b/tests/Cli/UseTest.php new file mode 100644 index 0000000..86b6b25 --- /dev/null +++ b/tests/Cli/UseTest.php @@ -0,0 +1,110 @@ +setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'using' ); + + $this->assertStringContainsString( + 'Using test-plugin', + $output, + 'The using command should report the set target.' + ); + } + + public function test_use_invalid_target_shows_error(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'use nonexistent-plugin', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'is not a valid target', + $output, + 'Using a nonexistent plugin should show an invalid target error.' + ); + } + + public function test_use_lists_available_targets(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'use nonexistent-plugin', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'test-plugin', + $output, + 'The error output should list available targets.' + ); + } + + public function test_use_with_subdir_target(): void { + $pluginsDir = $this->setUpPluginsDir( 'my-plugin' ); + + // Add a subdirectory target inside the plugin. + mkdir( $pluginsDir . '/my-plugin/common', 0777, true ); + file_put_contents( + $pluginsDir . '/my-plugin/common/plugin.php', + "/**\n* Plugin Name: Common\n*/" + ); + + $this->slicExec( 'use my-plugin/common', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'using' ); + + $this->assertStringContainsString( + 'Using my-plugin/common', + $output, + 'The using command should report the subdirectory target.' + ); + } + + public function test_use_changes_target(): void { + $pluginsDir = $this->setUpPluginsDir( 'first-plugin' ); + + // Add a second plugin to the plugins directory. + mkdir( $pluginsDir . '/second-plugin', 0777, true ); + file_put_contents( + $pluginsDir . '/second-plugin/plugin.php', + "/**\n* Plugin Name: Second Plugin\n*/" + ); + + $this->slicExec( 'use first-plugin', $this->dockerMockEnv() ); + $this->slicExec( 'use second-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'using' ); + + $this->assertStringContainsString( + 'Using second-plugin', + $output, + 'The using command should report the most recently set target.' + ); + } + + public function test_use_without_here_shows_error(): void { + // Set up a plugins directory without running 'here' to register it. + $tempDir = sys_get_temp_dir() . '/slic-no-here-' . uniqid( '', true ); + mkdir( $tempDir, 0777, true ); + mkdir( $tempDir . '/test-plugin', 0777, true ); + file_put_contents( + $tempDir . '/test-plugin/plugin.php', + "/**\n* Plugin Name: Test Plugin\n*/" + ); + chdir( $tempDir ); + + $output = $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'is not a valid target', + $output, + 'Using a plugin without a configured here directory should show an error.' + ); + + chdir( $this->initialDir ); + exec( 'rm -rf ' . escapeshellarg( $tempDir ) ); + } +} diff --git a/tests/Cli/UsingTest.php b/tests/Cli/UsingTest.php new file mode 100644 index 0000000..7741cba --- /dev/null +++ b/tests/Cli/UsingTest.php @@ -0,0 +1,52 @@ +setUpPluginsDir( 'test-plugin' ); + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'using', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'test-plugin', + $output, + 'The using command should display the current target.' + ); + } + + public function test_using_with_no_target_shows_none(): void { + $this->setUpPluginsDir( 'test-plugin' ); + + $output = $this->slicExec( 'using', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'not using any target', + $output, + 'The using command should indicate no target is set.' + ); + } + + public function test_using_after_target_change(): void { + $pluginsDir = $this->setUpPluginsDir( 'plugin-alpha' ); + // Add a second plugin to the plugins directory. + mkdir( $pluginsDir . '/plugin-beta', 0777, true ); + file_put_contents( + $pluginsDir . '/plugin-beta/plugin.php', + "/**\n* Plugin Name: Plugin Beta\n*/" + ); + + $this->slicExec( 'use plugin-alpha', $this->dockerMockEnv() ); + $this->slicExec( 'use plugin-beta', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'using', $this->dockerMockEnv() ); + + $this->assertStringContainsString( + 'plugin-beta', + $output, + 'The using command should display the most recently set target.' + ); + } +} diff --git a/tests/Cli/WorktreeTest.php b/tests/Cli/WorktreeTest.php new file mode 100644 index 0000000..52d5a33 --- /dev/null +++ b/tests/Cli/WorktreeTest.php @@ -0,0 +1,38 @@ +slicExec( 'worktree help' ); + + $this->assertStringContainsString( + 'USAGE', + $output, + 'The worktree help output should contain USAGE section.' + ); + + $this->assertStringContainsString( + 'SUMMARY', + $output, + 'The worktree help output should contain SUMMARY section.' + ); + } + + public function test_worktree_no_subcommand_shows_help(): void { + $output = $this->slicExec( 'worktree' ); + + $this->assertStringContainsString( + 'Available commands', + $output, + 'Running worktree without a subcommand should show available commands.' + ); + + $this->assertStringContainsString( + "Run 'slic worktree help' for more information.", + $output, + 'Running worktree without a subcommand should suggest running help.' + ); + } +} diff --git a/tests/Cli/WpCliTest.php b/tests/Cli/WpCliTest.php new file mode 100644 index 0000000..04e8b2a --- /dev/null +++ b/tests/Cli/WpCliTest.php @@ -0,0 +1,118 @@ +slicExec( 'wp help' ); + + $this->assertStringContainsString( + 'Runs a wp-cli command', + $output, + 'The wp help output should describe the command purpose.' + ); + + $this->assertStringContainsString( + 'USAGE:', + $output, + 'The wp help output should contain a USAGE section.' + ); + } + + public function test_wp_passes_commands_with_target(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'wp plugin list', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Unknown command', + $output, + 'The wp command should not produce an unknown command error.' + ); + } + + public function test_cli_is_alias_for_wp(): void { + $wpOutput = $this->slicExec( 'wp help' ); + $cliOutput = $this->slicExec( 'cli help' ); + + $this->assertStringContainsString( + 'Runs a wp-cli command', + $cliOutput, + 'The cli help output should describe the same command as wp.' + ); + + $this->assertStringContainsString( + 'USAGE:', + $cliOutput, + 'The cli help output should contain a USAGE section.' + ); + + $this->assertStringContainsString( + 'cli', + $cliOutput, + 'The cli help output should reference the cli command name.' + ); + + $this->assertEquals( + $wpOutput, + $cliOutput, + 'The wp and cli help outputs should be identical.' + ); + } + + public function test_site_cli_help_shows_usage(): void { + $output = $this->slicExec( 'site-cli help' ); + + $this->assertStringContainsString( + 'Waits for WordPress to be correctly set up to run a wp-cli command', + $output, + 'The site-cli help output should describe waiting for WordPress.' + ); + + $this->assertStringContainsString( + 'USAGE:', + $output, + 'The site-cli help output should contain a USAGE section.' + ); + } + + public function test_site_cli_passes_commands_with_target(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( 'site-cli plugin list', $this->dockerMockEnv() ); + + $this->assertStringNotContainsString( + 'Unknown command', + $output, + 'The site-cli command should not produce an unknown command error.' + ); + } + + public function test_site_cli_install_decline_exits_without_running(): void { + $this->setUpPluginsDir(); + + $this->slicExec( 'use test-plugin', $this->dockerMockEnv() ); + + $output = $this->slicExec( + 'site-cli _install', + $this->dockerMockEnv(), + "no\n" + ); + + $this->assertStringContainsString( + 'Do you really want to run it?', + $output, + 'The _install subcommand should prompt for confirmation.' + ); + $this->assertStringNotContainsString( + '--admin_user=admin', + $output, + 'Declining should prevent the install command from running.' + ); + } +} diff --git a/tests/Cli/XdebugTest.php b/tests/Cli/XdebugTest.php new file mode 100644 index 0000000..88aa740 --- /dev/null +++ b/tests/Cli/XdebugTest.php @@ -0,0 +1,120 @@ +setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug status' ); + + $this->assertStringContainsString( + 'XDebug status is:', + $output, + 'The xdebug status command should display the current XDebug state.' + ); + $this->assertStringContainsString( + 'Remote host:', + $output, + 'The xdebug status command should display the remote host.' + ); + $this->assertStringContainsString( + 'Remote port:', + $output, + 'The xdebug status command should display the remote port.' + ); + $this->assertStringContainsString( + 'IDE Key (server name):', + $output, + 'The xdebug status command should display the IDE key.' + ); + } + + public function test_xdebug_on_activates(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug on' ); + + $this->assertStringContainsString( + 'XDebug status: on', + $output, + 'The xdebug on command should confirm XDebug is activated.' + ); + } + + public function test_xdebug_off_deactivates(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug off' ); + + $this->assertStringContainsString( + 'XDebug status: off', + $output, + 'The xdebug off command should confirm XDebug is deactivated.' + ); + } + + public function test_xdebug_port_sets_port(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug port 9009' ); + + $this->assertStringContainsString( + 'Setting XDP=9009', + $output, + 'The xdebug port command should confirm the port is being set.' + ); + $this->assertStringContainsString( + 'Tear down the stack with down and restart it to apply the new settings!', + $output, + 'The xdebug port command should remind the user to restart.' + ); + } + + public function test_xdebug_host_sets_host(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug host 192.168.1.2' ); + + $this->assertStringContainsString( + 'Setting XDH=192.168.1.2', + $output, + 'The xdebug host command should confirm the host is being set.' + ); + $this->assertStringContainsString( + 'Tear down the stack with down and restart it to apply the new settings!', + $output, + 'The xdebug host command should remind the user to restart.' + ); + } + + public function test_xdebug_key_sets_key(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug key mykey' ); + + $this->assertStringContainsString( + 'Setting XDK=mykey', + $output, + 'The xdebug key command should confirm the IDE key is being set.' + ); + $this->assertStringContainsString( + 'Tear down the stack with down and restart it to apply the new settings!', + $output, + 'The xdebug key command should remind the user to restart.' + ); + } + + public function test_xdebug_no_args_shows_help_or_status(): void { + $this->setUpPluginsDir(); + + $output = $this->slicExec( 'xdebug' ); + + $this->assertStringContainsString( + 'XDebug status:', + $output, + 'Running xdebug with no arguments should show the current XDebug status.' + ); + } +} diff --git a/tests/_support/bin/docker-mock-host-ip b/tests/_support/bin/docker-mock-host-ip new file mode 100755 index 0000000..b0f9b06 --- /dev/null +++ b/tests/_support/bin/docker-mock-host-ip @@ -0,0 +1,9 @@ +#!/bin/bash +# A mock docker binary that returns a fake IP for host-ip commands. +for arg in "$@"; do + if [[ "$arg" == *"host-ip"* ]]; then + echo "172.17.0.1" + exit 0 + fi +done +exit 0 diff --git a/tests/_support/bin/git-mock-dir/git b/tests/_support/bin/git-mock-dir/git new file mode 100755 index 0000000..3b5705e --- /dev/null +++ b/tests/_support/bin/git-mock-dir/git @@ -0,0 +1,4 @@ +#!/bin/bash +# A mock git binary that always fails. Used to prevent real git clone calls from hanging in tests. +echo "fatal: mock git failure" >&2 +exit 128