diff --git a/.travis.yml b/.travis.yml index e3c0571b72..6154627854 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,8 @@ matrix: env: WP_VERSION=3.7.11 - php: 5.3 env: WP_VERSION=3.7.11 DEPLOY_BRANCH=master + - php: 7.1 + env: WP_VERSION=latest SAVE_COMMENTS_DISABLED=1 before_script: - export PATH="$HOME/.composer/vendor/bin:$PATH" diff --git a/ci/test.sh b/ci/test.sh index 2cace66435..0ec1967595 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -11,4 +11,9 @@ vendor/bin/phpunit BEHAT_TAGS=$(php ci/behat-tags.php) # Run the functional tests -vendor/bin/behat --format progress $BEHAT_TAGS --strict +if [[ -n "$SAVE_COMMENTS_DISABLED" ]]; then + # Run the functional tests with opcache.save_comments disabled. + WP_CLI_PHP_ARGS='-dopcache.enable_cli=1 -dopcache.save_comments=0' vendor/bin/behat --format progress "$BEHAT_TAGS&&~@require-opcache-save-comments" --strict +else + vendor/bin/behat --format progress $BEHAT_TAGS --strict +fi diff --git a/features/bootstrap.feature b/features/bootstrap.feature index e5af584b1c..d72c22bfad 100644 --- a/features/bootstrap.feature +++ b/features/bootstrap.feature @@ -1,5 +1,6 @@ Feature: Bootstrap WP-CLI + @require-opcache-save-comments Scenario: Basic Composer stack Given an empty directory And a composer.json file: diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 96e1647dfa..7dc5bfa564 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -8,6 +8,9 @@ use \WP_CLI\Process; use \WP_CLI\Utils; +use Symfony\Component\Filesystem\Filesystem, + Symfony\Component\Filesystem\Exception\IOExceptionInterface; + // Inside a community package if ( file_exists( __DIR__ . '/utils.php' ) ) { require_once __DIR__ . '/utils.php'; @@ -44,7 +47,7 @@ */ class FeatureContext extends BehatContext implements ClosuredContextInterface { - private static $cache_dir, $suite_cache_dir; + private static $cache_dir, $suite_cache_dir, $fs; private static $db_settings = array( 'dbname' => 'wp_cli_test', @@ -76,6 +79,9 @@ private static function get_process_env_variables() { if ( $term = getenv( 'TERM' ) ) { $env['TERM'] = $term; } + if ( $php_args = getenv( 'WP_CLI_PHP_ARGS' ) ) { + $env['WP_CLI_PHP_ARGS'] = $php_args; + } return $env; } @@ -113,7 +119,7 @@ public static function prepare( SuiteEvent $event ) { */ public static function afterSuite( SuiteEvent $event ) { if ( self::$suite_cache_dir ) { - Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run(); + self::$fs->remove( self::$suite_cache_dir ); } } @@ -131,13 +137,13 @@ public function afterScenario( $event ) { if ( isset( $this->variables['RUN_DIR'] ) ) { // remove altered WP install, unless there's an error if ( $event->getResult() < 4 && 0 === strpos( $this->variables['RUN_DIR'], sys_get_temp_dir() ) ) { - $this->proc( Utils\esc_cmd( 'rm -rf %s', $this->variables['RUN_DIR'] ) )->run(); + self::$fs->remove( $this->variables['RUN_DIR'] ); } } // Remove WP-CLI package directory if ( isset( $this->variables['PACKAGE_PATH'] ) ) { - $this->proc( Utils\esc_cmd( 'rm -rf %s', $this->variables['PACKAGE_PATH'] ) )->run(); + self::$fs->remove( $this->variables['PACKAGE_PATH'] ); } foreach ( $this->running_procs as $proc ) { @@ -192,6 +198,9 @@ public function __construct( array $parameters ) { $this->drop_db(); $this->set_cache_dir(); $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); + if ( ! self::$fs ) { + self::$fs = new FileSystem; + } } public function getStepDefinitionResources() { @@ -307,7 +316,9 @@ public function download_phar( $version = 'same' ) { private function set_cache_dir() { $path = sys_get_temp_dir() . '/wp-cli-test-cache'; - $this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check(); + if ( ! file_exists( $path ) ) { + mkdir( $path, 0777, true ); + } $this->variables['CACHE_DIR'] = $path; } @@ -375,6 +386,13 @@ public function move_files( $src, $dest ) { rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); } + /** + * Remove a directory (recursive). + */ + public function remove_dir( $dir ) { + self::$fs->remove( $dir ); + } + public function add_line_to_wp_config( &$wp_config_code, $line ) { $token = "/* That's all, stop editing!"; @@ -388,7 +406,7 @@ public function download_wp( $subdir = '' ) { mkdir( $dest_dir ); } - $this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check(); + self::$fs->mirror( self::$cache_dir, $dest_dir ); // disable emailing mkdir( $dest_dir . '/wp-content/mu-plugins' ); diff --git a/features/cli-info.feature b/features/cli-info.feature index b5cedc1457..19cd0a09e0 100644 --- a/features/cli-info.feature +++ b/features/cli-info.feature @@ -1,7 +1,12 @@ Feature: Review CLI information + Background: + When I run `wp package path` + Then save STDOUT as {PACKAGE_PATH} + Scenario: Get the path to the packages directory Given an empty directory + And an empty {PACKAGE_PATH} directory When I run `wp cli info --format=json` Then STDOUT should be JSON containing: diff --git a/features/cli.feature b/features/cli.feature index 4634b09ab7..f2413a7960 100644 --- a/features/cli.feature +++ b/features/cli.feature @@ -137,7 +137,7 @@ Feature: `wp cli` tasks y """ - When I run `{PHAR_PATH} cli check-update --minor --field=version` + When I run `{PHAR_PATH} cli check-update --field=version | head -1` Then STDOUT should not be empty And save STDOUT as {UPDATE_VERSION} diff --git a/features/steps/given.php b/features/steps/given.php index c785df1137..3e92c1b8f0 100644 --- a/features/steps/given.php +++ b/features/steps/given.php @@ -10,6 +10,12 @@ function ( $world ) { } ); +$steps->Given( '/^an empty ([^\s]+) directory$/', + function ( $world, $dir ) { + $world->remove_dir( $world->replace_variables( $dir ) ); + } +); + $steps->Given( '/^an empty cache/', function ( $world ) { $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); @@ -20,7 +26,10 @@ function ( $world ) { function ( $world, $path, PyStringNode $content ) { $content = (string) $content . "\n"; $full_path = $world->variables['RUN_DIR'] . "/$path"; - Process::create( \WP_CLI\utils\esc_cmd( 'mkdir -p %s', dirname( $full_path ) ) )->run_check(); + $dir = dirname( $full_path ); + if ( ! file_exists( $dir ) ) { + mkdir( $dir, 0777, true ); + } file_put_contents( $full_path, $content ); } ); diff --git a/php/WP_CLI/Dispatcher/CommandFactory.php b/php/WP_CLI/Dispatcher/CommandFactory.php index bd667fe052..8cf6f25d74 100644 --- a/php/WP_CLI/Dispatcher/CommandFactory.php +++ b/php/WP_CLI/Dispatcher/CommandFactory.php @@ -9,6 +9,9 @@ */ class CommandFactory { + // Cache of file contents, indexed by filename. Only used if opcache.save_comments is disabled. + private static $file_contents = array(); + /** * Create a new CompositeCommand (or Subcommand if class has __invoke()) * @@ -40,6 +43,13 @@ public static function create( $name, $callable, $parent ) { return $command; } + /** + * Clear the file contents cache. + */ + public static function clear_file_contents_cache() { + self::$file_contents = array(); + } + /** * Create a new Subcommand instance. * @@ -51,7 +61,8 @@ public static function create( $name, $callable, $parent ) { * @param string $method Class method to be called upon invocation. */ private static function create_subcommand( $parent, $name, $callable, $reflection ) { - $docparser = new \WP_CLI\DocParser( $reflection->getDocComment() ); + $doc_comment = self::get_doc_comment( $reflection ); + $docparser = new \WP_CLI\DocParser( $doc_comment ); if ( is_array( $callable ) ) { if ( !$name ) @@ -60,6 +71,9 @@ private static function create_subcommand( $parent, $name, $callable, $reflectio if ( !$name ) $name = $reflection->name; } + if ( ! $doc_comment ) { + \WP_CLI::debug( null === $doc_comment ? "Failed to get doc comment for {$name}." : "No doc comment for {$name}.", 'commandfactory' ); + } $when_invoked = function ( $args, $assoc_args ) use ( $callable ) { if ( is_array( $callable ) ) { @@ -82,7 +96,11 @@ private static function create_subcommand( $parent, $name, $callable, $reflectio */ private static function create_composite_command( $parent, $name, $callable ) { $reflection = new \ReflectionClass( $callable ); - $docparser = new \WP_CLI\DocParser( $reflection->getDocComment() ); + $doc_comment = self::get_doc_comment( $reflection ); + if ( ! $doc_comment ) { + \WP_CLI::debug( null === $doc_comment ? "Failed to get doc comment for {$name}." : "No doc comment for {$name}.", 'commandfactory' ); + } + $docparser = new \WP_CLI\DocParser( $doc_comment ); $container = new CompositeCommand( $parent, $name, $docparser ); @@ -110,5 +128,67 @@ private static function create_composite_command( $parent, $name, $callable ) { private static function is_good_method( $method ) { return $method->isPublic() && !$method->isStatic() && 0 !== strpos( $method->getName(), '__' ); } -} + /** + * Gets the document comment. Caters for PHP directive `opcache.save comments` being disabled. + * + * @param ReflectionMethod|ReflectionClass|ReflectionFunction $reflection Reflection instance. + * @return string|false|null Doc comment string if any, false if none (same as `Reflection*::getDocComment()`), null if error. + */ + private static function get_doc_comment( $reflection ) { + $doc_comment = $reflection->getDocComment(); + + if ( false !== $doc_comment || ! ( ini_get( 'opcache.enable_cli' ) && ! ini_get( 'opcache.save_comments' ) ) ) { + // Either have doc comment, or no doc comment and save comments enabled - standard situation. + if ( ! getenv( 'WP_CLI_TEST_GET_DOC_COMMENT' ) ) { + return $doc_comment; + } + } + + $filename = $reflection->getFileName(); + + if ( isset( self::$file_contents[ $filename ] ) ) { + $contents = self::$file_contents[ $filename ]; + } elseif ( is_readable( $filename ) && ( $contents = file_get_contents( $filename ) ) ) { + self::$file_contents[ $filename ] = $contents = explode( "\n", $contents ); + } else { + \WP_CLI::debug( "Could not read contents for filename '{$filename}'.", 'commandfactory' ); + return null; + } + + return self::extract_last_doc_comment( implode( "\n", array_slice( $contents, 0, $reflection->getStartLine() ) ) ); + } + + /** + * Returns the last doc comment if any in `$content`. + * + * @param string $content The content, which should end at the class or function declaration. + * @return string|bool The last doc comment if any, or false if none. + */ + private static function extract_last_doc_comment( $content ) { + $content = trim( $content ); + $comment_end_pos = strrpos( $content, '*/' ); + if ( false === $comment_end_pos ) { + return false; + } + // Make sure comment end belongs to this class/function. + if ( preg_match_all( '/(?:^|[\s;}])(?:class|function)\s+/', substr( $content, $comment_end_pos + 2 ), $dummy /*needed for PHP 5.3*/ ) > 1 ) { + return false; + } + $content = substr( $content, 0, $comment_end_pos + 2 ); + if ( false === ( $comment_start_pos = strrpos( $content, '/**' ) ) || $comment_start_pos + 2 === $comment_end_pos ) { + return false; + } + // Make sure comment start belongs to this comment end. + if ( false !== ( $comment_end2_pos = strpos( substr( $content, $comment_start_pos ), '*/' ) ) && $comment_start_pos + $comment_end2_pos < $comment_end_pos ) { + return false; + } + // Allow for '/**' within doc comment. + $subcontent = substr( $content, 0, $comment_start_pos ); + while ( false !== ( $comment_start2_pos = strrpos( $subcontent, '/**' ) ) && false === strpos( $subcontent, '*/', $comment_start2_pos ) ) { + $comment_start_pos = $comment_start2_pos; + $subcontent = substr( $subcontent, 0, $comment_start_pos ); + } + return substr( $content, $comment_start_pos, $comment_end_pos + 2 ); + } +} diff --git a/php/utils.php b/php/utils.php index dd187cd6d5..0ab0747e10 100644 --- a/php/utils.php +++ b/php/utils.php @@ -568,7 +568,7 @@ function http_request( $method, $url, $data = null, $headers = array(), $options } } if ( empty( $options['verify'] ) ){ - WP_CLI::error_log( "Cannot find SSL certificate." ); + WP_CLI::error( "Cannot find SSL certificate." ); } } diff --git a/tests/data/commandfactory-doc_comment-class.php b/tests/data/commandfactory-doc_comment-class.php new file mode 100644 index 0000000000..01b1c5eb25 --- /dev/null +++ b/tests/data/commandfactory-doc_comment-class.php @@ -0,0 +1,70 @@ +] + * + * ## EXAMPLES + * + * $ wp foo command2 --path=/**a/**b/**c/** + */ + +final + protected + static + function + command2() { + } + + /** + * Command3 function + * + * ## OPTIONS + * + * [--path=] + * + * ## EXAMPLES + * + * $ wp foo command3 --path=/**a/**b/**c/** + function*/public function command3( $function ) {} + + function command4() {} +} + +/** + * Basic class + * + * ## EXAMPLES + * + * # Foo. + * $ wp foo --final abstract + class*/abstract class + CommandFactoryTests_Get_Doc_Comment_2_Command + extends WP_CLI_Command + { + function command1() {} + } diff --git a/tests/data/commandfactory-doc_comment-function.php b/tests/data/commandfactory-doc_comment-function.php new file mode 100644 index 0000000000..1130cb676a --- /dev/null +++ b/tests/data/commandfactory-doc_comment-function.php @@ -0,0 +1,19 @@ +setAccessible( true ); + } + + $actual = $extract_last_doc_comment->invoke( null, $content ); + $this->assertSame( $expected, $actual ); + } + + function dataProviderExtractLastDocComment() { + return array( + array( "", false ), + array( "*/", false ), + array( "/*/ ", false ), + array( "/**/", false ), + array( "/***/ */", false ), + array( "/***/", "/***/" ), + array( "\n /**\n \n \t\n */ \t\n \n ", "/**\n \n \t\n */" ), + array( "/**/ /***/ /***/", "/***/" ), + array( "asdfasdf/** /** */", "/** /** */" ), + array( "*//** /** */", "/** /** */" ), + array( "/** *//** /** */", "/** /** */" ), + array( "*//** */ /** /** */", "/** /** */" ), + array( "*//** *//** /** /** */", "/** /** /** */" ), + + array( "/** */class qwer", "/** */" ), + array( "/**1*/class qwer{}/**2*/class asdf", "/**2*/" ), + array( "/** */class qwer {}\nclass asdf", false ), + + array( "/** */function qwer", "/** */" ), + array( "/** */function qwer( \$function ) {}", "/** */" ), + array( "/**1*/function qwer() {}/**2*/function asdf()", "/**2*/" ), + array( "/** */function qwer() {}\nfunction asdf()", false ), + array( "/** */function qwer() {}function asdf()", false ), + array( "/** */function qwer() {};function asdf( \$function )", false ), + ); + } + + function testGetDocComment() { + // Save and set test env var. + $prev_test_get_doc_comment = getenv( 'WP_CLI_TEST_GET_DOC_COMMENT' ); + putenv( 'WP_CLI_TEST_GET_DOC_COMMENT=1' ); + + // Make private function accessible. + $get_doc_comment = new \ReflectionMethod( 'WP_CLI\Dispatcher\CommandFactory', 'get_doc_comment' ); + $get_doc_comment->setAccessible( true ); + + require __DIR__ . '/data/commandfactory-doc_comment-class.php'; + + // Class 1 + + $reflection = new \ReflectionClass( 'CommandFactoryTests_Get_Doc_Comment_1_Command' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Class method 1 + + $reflection = new \ReflectionMethod( 'CommandFactoryTests_Get_Doc_Comment_1_Command', 'command1' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Class method 2 + + $reflection = new \ReflectionMethod( 'CommandFactoryTests_Get_Doc_Comment_1_Command', 'command2' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Class method 3 + + $reflection = new \ReflectionMethod( 'CommandFactoryTests_Get_Doc_Comment_1_Command', 'command3' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Class method 4 + + $reflection = new \ReflectionMethod( 'CommandFactoryTests_Get_Doc_Comment_1_Command', 'command4' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + $this->assertFalse( $actual ); + + // Class 2 + + $reflection = new \ReflectionClass( 'CommandFactoryTests_Get_Doc_Comment_2_Command' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Class method 1 + + $reflection = new \ReflectionMethod( 'CommandFactoryTests_Get_Doc_Comment_2_Command', 'command1' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + $this->assertFalse( $actual ); + + // Functions + + require __DIR__ . '/data/commandfactory-doc_comment-function.php'; + + // Function 1 + + $reflection = new \ReflectionFunction( 'commandfactorytests_get_doc_comment_func_1' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Function 2 + + $reflection = new \ReflectionFunction( 'commandfactorytests_get_doc_comment_func_2' ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Function 3 + + $reflection = new \ReflectionFunction( $commandfactorytests_get_doc_comment_func_3 ); + $expected = $reflection->getDocComment(); + + $actual = $get_doc_comment->invoke( null, $reflection ); + $this->assertSame( $expected, $actual ); + + // Restore. + + putenv( false === $prev_test_get_doc_comment ? 'WP_CLI_TEST_GET_DOC_COMMENT' : "WP_CLI_TEST_GET_DOC_COMMENT=$prev_test_get_doc_comment" ); + $this->assertTrue( true ); + } +}