diff options
author | Andrew Nacin <nacin@git.wordpress.org> | 2013-08-29 18:39:34 +0000 |
---|---|---|
committer | Andrew Nacin <nacin@git.wordpress.org> | 2013-08-29 18:39:34 +0000 |
commit | 8045afd81b7c80f6ef5b327c115a5bbb43e4b65c (patch) | |
tree | 15d457007610c451577debda89bd9e9cd3d74551 /tests/phpunit/includes | |
parent | d34baebc1d8111c9c1014e11001957face778e52 (diff) | |
download | wordpress-8045afd81b7c80f6ef5b327c115a5bbb43e4b65c.tar.gz wordpress-8045afd81b7c80f6ef5b327c115a5bbb43e4b65c.zip |
Move PHPUnit tests into a tests/phpunit directory.
wp-tests-config.php can/should reside in the root of a develop checkout. `phpunit` should be run from the root.
see #25088.
git-svn-id: https://develop.svn.wordpress.org/trunk@25165 602fd350-edb4-49c9-b593-d223f7449a82
Diffstat (limited to 'tests/phpunit/includes')
-rw-r--r-- | tests/phpunit/includes/bootstrap.php | 135 | ||||
-rw-r--r-- | tests/phpunit/includes/exceptions.php | 33 | ||||
-rw-r--r-- | tests/phpunit/includes/factory.php | 344 | ||||
-rw-r--r-- | tests/phpunit/includes/functions.php | 44 | ||||
-rw-r--r-- | tests/phpunit/includes/install.php | 67 | ||||
-rw-r--r-- | tests/phpunit/includes/mock-fs.php | 226 | ||||
-rw-r--r-- | tests/phpunit/includes/mock-image-editor.php | 43 | ||||
-rw-r--r-- | tests/phpunit/includes/mock-mailer.php | 26 | ||||
-rw-r--r-- | tests/phpunit/includes/testcase-ajax.php | 182 | ||||
-rw-r--r-- | tests/phpunit/includes/testcase-xmlrpc.php | 30 | ||||
-rw-r--r-- | tests/phpunit/includes/testcase.php | 227 | ||||
-rw-r--r-- | tests/phpunit/includes/trac.php | 54 | ||||
-rw-r--r-- | tests/phpunit/includes/utils.php | 365 | ||||
-rw-r--r-- | tests/phpunit/includes/wp-profiler.php | 216 |
14 files changed, 1992 insertions, 0 deletions
diff --git a/tests/phpunit/includes/bootstrap.php b/tests/phpunit/includes/bootstrap.php new file mode 100644 index 0000000000..b73dd4e09c --- /dev/null +++ b/tests/phpunit/includes/bootstrap.php @@ -0,0 +1,135 @@ +<?php +/** + * Installs WordPress for running the tests and loads WordPress and the test libraries + */ + + +$config_file_path = dirname( dirname( __FILE__ ) ); +if ( ! file_exists( $config_file_path . '/wp-tests-config.php' ) ) { + // Support the config file from the root of the develop repository. + if ( basename( $config_file_path ) === 'phpunit' && basename( dirname( $config_file_path ) ) === 'tests' ) + $config_file_path = dirname( dirname( $config_file_path ) ); +} +$config_file_path .= '/wp-tests-config.php'; + +/* + * Globalize some WordPress variables, because PHPUnit loads this file inside a function + * See: https://github.com/sebastianbergmann/phpunit/issues/325 + */ +global $wpdb, $current_site, $current_blog, $wp_rewrite, $shortcode_tags, $wp, $phpmailer; + +if ( !is_readable( $config_file_path ) ) { + die( "ERROR: wp-tests-config.php is missing! Please use wp-tests-config-sample.php to create a config file.\n" ); +} +require_once $config_file_path; + +define( 'DIR_TESTDATA', dirname( __FILE__ ) . '/../data' ); + +if ( ! defined( 'WP_TESTS_FORCE_KNOWN_BUGS' ) ) + define( 'WP_TESTS_FORCE_KNOWN_BUGS', false ); + +// Cron tries to make an HTTP request to the blog, which always fails, because tests are run in CLI mode only +define( 'DISABLE_WP_CRON', true ); + +define( 'WP_MEMORY_LIMIT', -1 ); +define( 'WP_MAX_MEMORY_LIMIT', -1 ); + +$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; +$_SERVER['HTTP_HOST'] = WP_TESTS_DOMAIN; +$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php'; + +if ( "1" == getenv( 'WP_MULTISITE' ) || + ( defined( 'WP_TESTS_MULTISITE') && WP_TESTS_MULTISITE ) ) { + $multisite = true; +} else { + $multisite = false; +} + +// Override the PHPMailer +require_once( dirname( __FILE__ ) . '/mock-mailer.php' ); +$phpmailer = new MockPHPMailer(); + +system( WP_PHP_BINARY . ' ' . escapeshellarg( dirname( __FILE__ ) . '/install.php' ) . ' ' . escapeshellarg( $config_file_path ) . ' ' . $multisite ); + +if ( $multisite ) { + echo "Running as multisite..." . PHP_EOL; + define( 'MULTISITE', true ); + define( 'SUBDOMAIN_INSTALL', false ); + define( 'DOMAIN_CURRENT_SITE', WP_TESTS_DOMAIN ); + define( 'PATH_CURRENT_SITE', '/' ); + define( 'SITE_ID_CURRENT_SITE', 1 ); + define( 'BLOG_ID_CURRENT_SITE', 1 ); + $GLOBALS['base'] = '/'; +} else { + echo "Running as single site... To run multisite, use -c multisite.xml" . PHP_EOL; +} +unset( $multisite ); + +require_once dirname( __FILE__ ) . '/functions.php'; + +// Preset WordPress options defined in bootstrap file. +// Used to activate themes, plugins, as well as other settings. +if(isset($GLOBALS['wp_tests_options'])) { + function wp_tests_options( $value ) { + $key = substr( current_filter(), strlen( 'pre_option_' ) ); + return $GLOBALS['wp_tests_options'][$key]; + } + + foreach ( array_keys( $GLOBALS['wp_tests_options'] ) as $key ) { + tests_add_filter( 'pre_option_'.$key, 'wp_tests_options' ); + } +} + +// Load WordPress +require_once ABSPATH . '/wp-settings.php'; + +// Delete any default posts & related data +_delete_all_posts(); + +require dirname( __FILE__ ) . '/testcase.php'; +require dirname( __FILE__ ) . '/testcase-xmlrpc.php'; +require dirname( __FILE__ ) . '/testcase-ajax.php'; +require dirname( __FILE__ ) . '/exceptions.php'; +require dirname( __FILE__ ) . '/utils.php'; + +/** + * A child class of the PHP test runner. + * + * Not actually used as a runner. Rather, used to access the protected + * longOptions property, to parse the arguments passed to the script. + * + * If it is determined that phpunit was called with a --group that corresponds + * to an @ticket annotation (such as `phpunit --group 12345` for bugs marked + * as #WP12345), then it is assumed that known bugs should not be skipped. + * + * If WP_TESTS_FORCE_KNOWN_BUGS is already set in wp-tests-config.php, then + * how you call phpunit has no effect. + */ +class WP_PHPUnit_TextUI_Command extends PHPUnit_TextUI_Command { + function __construct( $argv ) { + $options = PHPUnit_Util_Getopt::getopt( + $argv, + 'd:c:hv', + array_keys( $this->longOptions ) + ); + $ajax_message = true; + foreach ( $options[0] as $option ) { + switch ( $option[0] ) { + case '--exclude-group' : + $ajax_message = false; + continue 2; + case '--group' : + $groups = explode( ',', $option[1] ); + foreach ( $groups as $group ) { + if ( is_numeric( $group ) || preg_match( '/^(UT|Plugin)\d+$/', $group ) ) + WP_UnitTestCase::forceTicket( $group ); + } + $ajax_message = ! in_array( 'ajax', $groups ); + continue 2; + } + } + if ( $ajax_message ) + echo "Not running ajax tests... To execute these, use --group ajax." . PHP_EOL; + } +} +new WP_PHPUnit_TextUI_Command( $_SERVER['argv'] ); diff --git a/tests/phpunit/includes/exceptions.php b/tests/phpunit/includes/exceptions.php new file mode 100644 index 0000000000..50976fb0b3 --- /dev/null +++ b/tests/phpunit/includes/exceptions.php @@ -0,0 +1,33 @@ +<?php + +class WP_Tests_Exception extends PHPUnit_Framework_Exception { + +} + +/** + * General exception for wp_die() + */ +class WPDieException extends Exception {} + +/** + * Exception for cases of wp_die(), for ajax tests. + * This means there was an error (no output, and a call to wp_die) + * + * @package WordPress + * @subpackage Unit Tests + * @since 3.4.0 + */ +class WPAjaxDieStopException extends WPDieException {} + +/** + * Exception for cases of wp_die(), for ajax tests. + * This means execution of the ajax function should be halted, but the unit + * test can continue. The function finished normally and there was not an + * error (output happened, but wp_die was called to end execution) This is + * used with WP_Ajax_Response::send + * + * @package WordPress + * @subpackage Unit Tests + * @since 3.4.0 + */ +class WPAjaxDieContinueException extends WPDieException {} diff --git a/tests/phpunit/includes/factory.php b/tests/phpunit/includes/factory.php new file mode 100644 index 0000000000..bf5ba53777 --- /dev/null +++ b/tests/phpunit/includes/factory.php @@ -0,0 +1,344 @@ +<?php + +class WP_UnitTest_Factory { + + /** + * @var WP_UnitTest_Factory_For_Post + */ + public $post; + + /** + * @var WP_UnitTest_Factory_For_Attachment + */ + public $attachment; + + /** + * @var WP_UnitTest_Factory_For_Comment + */ + public $comment; + + /** + * @var WP_UnitTest_Factory_For_User + */ + public $user; + + /** + * @var WP_UnitTest_Factory_For_Term + */ + public $term; + + /** + * @var WP_UnitTest_Factory_For_Term + */ + public $category; + + /** + * @var WP_UnitTest_Factory_For_Term + */ + public $tag; + + /** + * @var WP_UnitTest_Factory_For_Blog + */ + public $blog; + + function __construct() { + $this->post = new WP_UnitTest_Factory_For_Post( $this ); + $this->attachment = new WP_UnitTest_Factory_For_Attachment( $this ); + $this->comment = new WP_UnitTest_Factory_For_Comment( $this ); + $this->user = new WP_UnitTest_Factory_For_User( $this ); + $this->term = new WP_UnitTest_Factory_For_Term( $this ); + $this->category = new WP_UnitTest_Factory_For_Term( $this, 'category' ); + $this->tag = new WP_UnitTest_Factory_For_Term( $this, 'post_tag' ); + if ( is_multisite() ) + $this->blog = new WP_UnitTest_Factory_For_Blog( $this ); + } +} + +class WP_UnitTest_Factory_For_Post extends WP_UnitTest_Factory_For_Thing { + + function __construct( $factory = null ) { + parent::__construct( $factory ); + $this->default_generation_definitions = array( + 'post_status' => 'publish', + 'post_title' => new WP_UnitTest_Generator_Sequence( 'Post title %s' ), + 'post_content' => new WP_UnitTest_Generator_Sequence( 'Post content %s' ), + 'post_excerpt' => new WP_UnitTest_Generator_Sequence( 'Post excerpt %s' ), + 'post_type' => 'post' + ); + } + + function create_object( $args ) { + return wp_insert_post( $args ); + } + + function update_object( $post_id, $fields ) { + $fields['ID'] = $post_id; + return wp_update_post( $fields ); + } + + function get_object_by_id( $post_id ) { + return get_post( $post_id ); + } +} + +class WP_UnitTest_Factory_For_Attachment extends WP_UnitTest_Factory_For_Post { + + function create_object( $file, $parent = 0, $args = array() ) { + return wp_insert_attachment( $args, $file, $parent ); + } +} + +class WP_UnitTest_Factory_For_User extends WP_UnitTest_Factory_For_Thing { + + function __construct( $factory = null ) { + parent::__construct( $factory ); + $this->default_generation_definitions = array( + 'user_login' => new WP_UnitTest_Generator_Sequence( 'User %s' ), + 'user_pass' => 'password', + 'user_email' => new WP_UnitTest_Generator_Sequence( 'user_%s@example.org' ), + ); + } + + function create_object( $args ) { + return wp_insert_user( $args ); + } + + function update_object( $user_id, $fields ) { + $fields['ID'] = $user_id; + return wp_update_user( $fields ); + } + + function get_object_by_id( $user_id ) { + return new WP_User( $user_id ); + } +} + +class WP_UnitTest_Factory_For_Comment extends WP_UnitTest_Factory_For_Thing { + + function __construct( $factory = null ) { + parent::__construct( $factory ); + $this->default_generation_definitions = array( + 'comment_author' => new WP_UnitTest_Generator_Sequence( 'Commenter %s' ), + 'comment_author_url' => new WP_UnitTest_Generator_Sequence( 'http://example.com/%s/' ), + 'comment_approved' => 1, + ); + } + + function create_object( $args ) { + return wp_insert_comment( $this->addslashes_deep( $args ) ); + } + + function update_object( $comment_id, $fields ) { + $fields['comment_ID'] = $comment_id; + return wp_update_comment( $this->addslashes_deep( $fields ) ); + } + + function create_post_comments( $post_id, $count = 1, $args = array(), $generation_definitions = null ) { + $args['comment_post_ID'] = $post_id; + return $this->create_many( $count, $args, $generation_definitions ); + } + + function get_object_by_id( $comment_id ) { + return get_comment( $comment_id ); + } +} + +class WP_UnitTest_Factory_For_Blog extends WP_UnitTest_Factory_For_Thing { + + function __construct( $factory = null ) { + global $current_site, $base; + parent::__construct( $factory ); + $this->default_generation_definitions = array( + 'domain' => $current_site->domain, + 'path' => new WP_UnitTest_Generator_Sequence( $base . 'testpath%s' ), + 'title' => new WP_UnitTest_Generator_Sequence( 'Site %s' ), + 'site_id' => $current_site->id, + ); + } + + function create_object( $args ) { + $meta = isset( $args['meta'] ) ? $args['meta'] : array(); + $user_id = isset( $args['user_id'] ) ? $args['user_id'] : get_current_user_id(); + return wpmu_create_blog( $args['domain'], $args['path'], $args['title'], $user_id, $meta, $args['site_id'] ); + } + + function update_object( $blog_id, $fields ) {} + + function get_object_by_id( $blog_id ) { + return get_blog_details( $blog_id, false ); + } +} + + +class WP_UnitTest_Factory_For_Term extends WP_UnitTest_Factory_For_Thing { + + private $taxonomy; + const DEFAULT_TAXONOMY = 'post_tag'; + + function __construct( $factory = null, $taxonomy = null ) { + parent::__construct( $factory ); + $this->taxonomy = $taxonomy ? $taxonomy : self::DEFAULT_TAXONOMY; + $this->default_generation_definitions = array( + 'name' => new WP_UnitTest_Generator_Sequence( 'Term %s' ), + 'taxonomy' => $this->taxonomy, + 'description' => new WP_UnitTest_Generator_Sequence( 'Term description %s' ), + ); + } + + function create_object( $args ) { + $args = array_merge( array( 'taxonomy' => $this->taxonomy ), $args ); + $term_id_pair = wp_insert_term( $args['name'], $args['taxonomy'], $args ); + if ( is_wp_error( $term_id_pair ) ) + return $term_id_pair; + return $term_id_pair['term_id']; + } + + function update_object( $term, $fields ) { + $fields = array_merge( array( 'taxonomy' => $this->taxonomy ), $fields ); + if ( is_object( $term ) ) + $taxonomy = $term->taxonomy; + $term_id_pair = wp_update_term( $term, $taxonomy, $fields ); + return $term_id_pair['term_id']; + } + + function add_post_terms( $post_id, $terms, $taxonomy, $append = true ) { + return wp_set_post_terms( $post_id, $terms, $taxonomy, $append ); + } + + function get_object_by_id( $term_id ) { + return get_term( $term_id, $this->taxonomy ); + } +} + +abstract class WP_UnitTest_Factory_For_Thing { + + var $default_generation_definitions; + var $factory; + + /** + * Creates a new factory, which will create objects of a specific Thing + * + * @param object $factory Global factory that can be used to create other objects on the system + * @param array $default_generation_definitions Defines what default values should the properties of the object have. The default values + * can be generators -- an object with next() method. There are some default generators: {@link WP_UnitTest_Generator_Sequence}, + * {@link WP_UnitTest_Generator_Locale_Name}, {@link WP_UnitTest_Factory_Callback_After_Create}. + */ + function __construct( $factory, $default_generation_definitions = array() ) { + $this->factory = $factory; + $this->default_generation_definitions = $default_generation_definitions; + } + + abstract function create_object( $args ); + abstract function update_object( $object, $fields ); + + function create( $args = array(), $generation_definitions = null ) { + if ( is_null( $generation_definitions ) ) + $generation_definitions = $this->default_generation_definitions; + + $generated_args = $this->generate_args( $args, $generation_definitions, $callbacks ); + $created = $this->create_object( $generated_args ); + if ( !$created || is_wp_error( $created ) ) + return $created; + + if ( $callbacks ) { + $updated_fields = $this->apply_callbacks( $callbacks, $created ); + $save_result = $this->update_object( $created, $updated_fields ); + if ( !$save_result || is_wp_error( $save_result ) ) + return $save_result; + } + return $created; + } + + function create_and_get( $args = array(), $generation_definitions = null ) { + $object_id = $this->create( $args, $generation_definitions ); + return $this->get_object_by_id( $object_id ); + } + + abstract function get_object_by_id( $object_id ); + + function create_many( $count, $args = array(), $generation_definitions = null ) { + $results = array(); + for ( $i = 0; $i < $count; $i++ ) { + $results[] = $this->create( $args, $generation_definitions ); + } + return $results; + } + + function generate_args( $args = array(), $generation_definitions = null, &$callbacks = null ) { + $callbacks = array(); + if ( is_null( $generation_definitions ) ) + $generation_definitions = $this->default_generation_definitions; + + foreach( array_keys( $generation_definitions ) as $field_name ) { + if ( !isset( $args[$field_name] ) ) { + $generator = $generation_definitions[$field_name]; + if ( is_scalar( $generator ) ) + $args[$field_name] = $generator; + elseif ( is_object( $generator ) && method_exists( $generator, 'call' ) ) { + $callbacks[$field_name] = $generator; + } elseif ( is_object( $generator ) ) + $args[$field_name] = $generator->next(); + else + return new WP_Error( 'invalid_argument', 'Factory default value should be either a scalar or an generator object.' ); + } + } + return $args; + } + + function apply_callbacks( $callbacks, $created ) { + $updated_fields = array(); + foreach( $callbacks as $field_name => $generator ) { + $updated_fields[$field_name] = $generator->call( $created ); + } + return $updated_fields; + } + + function callback( $function ) { + return new WP_UnitTest_Factory_Callback_After_Create( $function ); + } + + function addslashes_deep($value) { + if ( is_array( $value ) ) { + $value = array_map( array( $this, 'addslashes_deep' ), $value ); + } elseif ( is_object( $value ) ) { + $vars = get_object_vars( $value ); + foreach ($vars as $key=>$data) { + $value->{$key} = $this->addslashes_deep( $data ); + } + } elseif ( is_string( $value ) ) { + $value = addslashes( $value ); + } + + return $value; + } + +} + +class WP_UnitTest_Generator_Sequence { + var $next; + var $template_string; + + function __construct( $template_string = '%s', $start = 1 ) { + $this->next = $start; + $this->template_string = $template_string; + } + + function next() { + $generated = sprintf( $this->template_string , $this->next ); + $this->next++; + return $generated; + } +} + +class WP_UnitTest_Factory_Callback_After_Create { + var $callback; + + function __construct( $callback ) { + $this->callback = $callback; + } + + function call( $object ) { + return call_user_func( $this->callback, $object ); + } +} diff --git a/tests/phpunit/includes/functions.php b/tests/phpunit/includes/functions.php new file mode 100644 index 0000000000..5759d530c9 --- /dev/null +++ b/tests/phpunit/includes/functions.php @@ -0,0 +1,44 @@ +<?php + +// For adding hooks before loading WP +function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) { + global $wp_filter, $merged_filters; + + $idx = _test_filter_build_unique_id($tag, $function_to_add, $priority); + $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args); + unset( $merged_filters[ $tag ] ); + return true; +} + +function _test_filter_build_unique_id($tag, $function, $priority) { + global $wp_filter; + static $filter_id_count = 0; + + if ( is_string($function) ) + return $function; + + if ( is_object($function) ) { + // Closures are currently implemented as objects + $function = array( $function, '' ); + } else { + $function = (array) $function; + } + + if (is_object($function[0]) ) { + return spl_object_hash($function[0]) . $function[1]; + } else if ( is_string($function[0]) ) { + // Static Calling + return $function[0].$function[1]; + } +} + +function _delete_all_posts() { + global $wpdb; + + $all_posts = $wpdb->get_col("SELECT ID from {$wpdb->posts}"); + if ($all_posts) { + foreach ($all_posts as $id) + wp_delete_post( $id, true ); + } +} + diff --git a/tests/phpunit/includes/install.php b/tests/phpunit/includes/install.php new file mode 100644 index 0000000000..63de9d2185 --- /dev/null +++ b/tests/phpunit/includes/install.php @@ -0,0 +1,67 @@ +<?php +/** + * Installs WordPress for the purpose of the unit-tests + * + * @todo Reuse the init/load code in init.php + */ +error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT ); + +$config_file_path = $argv[1]; +$multisite = ! empty( $argv[2] ); + +define( 'WP_INSTALLING', true ); +require_once $config_file_path; +require_once dirname( __FILE__ ) . '/functions.php'; + +$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; +$_SERVER['HTTP_HOST'] = WP_TESTS_DOMAIN; +$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php'; + +require_once ABSPATH . '/wp-settings.php'; + +require_once ABSPATH . '/wp-admin/includes/upgrade.php'; +require_once ABSPATH . '/wp-includes/wp-db.php'; + +define( 'WP_TESTS_VERSION_FILE', ABSPATH . '.wp-tests-version' ); + +$wpdb->suppress_errors(); +$installed = $wpdb->get_var( "SELECT option_value FROM $wpdb->options WHERE option_name = 'siteurl'" ); +$wpdb->suppress_errors( false ); + +$hash = get_option( 'db_version' ) . ' ' . (int) $multisite . ' ' . sha1_file( $config_file_path ); + +if ( $installed && file_exists( WP_TESTS_VERSION_FILE ) && file_get_contents( WP_TESTS_VERSION_FILE ) == $hash ) + return; + +$wpdb->query( 'SET storage_engine = INNODB' ); +$wpdb->select( DB_NAME, $wpdb->dbh ); + +echo "Installing..." . PHP_EOL; + +foreach ( $wpdb->tables() as $table => $prefixed_table ) { + $wpdb->query( "DROP TABLE IF EXISTS $prefixed_table" ); +} + +foreach ( $wpdb->tables( 'ms_global' ) as $table => $prefixed_table ) { + $wpdb->query( "DROP TABLE IF EXISTS $prefixed_table" ); + + // We need to create references to ms global tables. + if ( $multisite ) + $wpdb->$table = $prefixed_table; +} + +wp_install( WP_TESTS_TITLE, 'admin', WP_TESTS_EMAIL, true, null, 'password' ); + +if ( $multisite ) { + echo "Installing network..." . PHP_EOL; + + define( 'WP_INSTALLING_NETWORK', true ); + + $title = WP_TESTS_TITLE . ' Network'; + $subdomain_install = false; + + install_network(); + populate_network( 1, WP_TESTS_DOMAIN, WP_TESTS_EMAIL, $title, '/', $subdomain_install ); +} + +file_put_contents( WP_TESTS_VERSION_FILE, $hash ); diff --git a/tests/phpunit/includes/mock-fs.php b/tests/phpunit/includes/mock-fs.php new file mode 100644 index 0000000000..3a99f8ba97 --- /dev/null +++ b/tests/phpunit/includes/mock-fs.php @@ -0,0 +1,226 @@ +<?php +class WP_Filesystem_MockFS extends WP_Filesystem_Base { + private $cwd; + + // Holds a array of objects which contain an array of objects, etc. + private $fs = null; + + // Holds a array of /path/to/file.php and /path/to/dir/ map to an object in $fs above + // a fast more efficient way of determining if a path exists, and access to that node + private $fs_map = array(); + + public $verbose = false; // Enable to debug WP_Filesystem_Base::find_folder() / etc. + public $errors = array(); + public $method = 'MockFS'; + + function __construct() {} + + function connect() { + return true; + } + + // Copy of core's function, but accepts a path. + function abspath( $path = false ) { + if ( ! $path ) + $path = ABSPATH; + $folder = $this->find_folder( $path ); + + // Perhaps the FTP folder is rooted at the WordPress install, Check for wp-includes folder in root, Could have some false positives, but rare. + if ( ! $folder && $this->is_dir('/wp-includes') ) + $folder = '/'; + return $folder; + } + + // Mock FS specific functions: + + /** + * Sets initial filesystem environment and/or clears the current environment. + * Can also be passed the initial filesystem to be setup which is passed to self::setfs() + */ + function init( $paths = '', $home_dir = '/' ) { + $this->fs = new MockFS_Directory_Node( '/' ); + $this->fs_map = array( + '/' => $this->fs, + ); + $this->cache = array(); // Used by find_folder() and friends + $this->cwd = isset( $this->fs_map[ $home_dir ] ) ? $this->fs_map[ $home_dir ] : '/'; + $this->setfs( $paths ); + } + + /** + * "Bulk Loads" a filesystem into the internal virtual filesystem + */ + function setfs( $paths ) { + if ( ! is_array($paths) ) + $paths = explode( "\n", $paths ); + + $paths = array_filter( array_map( 'trim', $paths ) ); + + foreach ( $paths as $path ) { + // Allow for comments + if ( '#' == $path[0] ) + continue; + + // Directories + if ( '/' == $path[ strlen($path) -1 ] ) + $this->mkdir( $path ); + else // Files (with dummy content for now) + $this->put_contents( $path, 'This is a test file' ); + } + + } + + /** + * Locates a filesystem "node" + */ + private function locate_node( $path ) { + return isset( $this->fs_map[ $path ] ) ? $this->fs_map[ $path ] : false; + } + + /** + * Locates a filesystem node for the parent of the given item + */ + private function locate_parent_node( $path ) { + return $this->locate_node( trailingslashit( dirname( $path ) ) ); + } + + // Here starteth the WP_Filesystem functions. + + function mkdir( $path, /* Optional args are ignored */ $chmod = false, $chown = false, $chgrp = false ) { + $path = trailingslashit( $path ); + + $parent_node = $this->locate_parent_node( $path ); + if ( ! $parent_node ) { + $this->mkdir( dirname( $path ) ); + $parent_node = $this->locate_parent_node( $path ); + if ( ! $parent_node ) + return false; + } + + $node = new MockFS_Directory_Node( $path ); + + $parent_node->children[ $node->name ] = $node; + $this->fs_map[ $path ] = $node; + + return true; + } + + function put_contents( $path, $contents = '', $mode = null ) { + if ( ! $this->is_dir( dirname( $path ) ) ) + $this->mkdir( dirname( $path ) ); + + $parent = $this->locate_parent_node( $path ); + $new_file = new MockFS_File_Node( $path, $contents ); + + $parent->children[ $new_file->name ] = $new_file; + $this->fs_map[ $path ] = $new_file; + } + + function get_contents( $file ) { + if ( ! $this->is_file( $file ) ) + return false; + return $this->fs_map[ $file ]->contents; + } + + function cwd() { + return $this->cwd->path; + } + + function chdir( $path ) { + if ( ! isset( $this->fs_map[ $path ] ) ) + return false; + + $this->cwd = $this->fs_map[ $path ]; + return true; + } + + function exists( $path ) { + return isset( $this->fs_map[ $path ] ) || isset( $this->fs_map[ trailingslashit( $path ) ] ); + } + + function is_file( $file ) { + return isset( $this->fs_map[ $file ] ) && $this->fs_map[ $file ]->is_file(); + } + + function is_dir( $path ) { + $path = trailingslashit( $path ); + + return isset( $this->fs_map[ $path ] ) && $this->fs_map[ $path ]->is_dir(); + } + + function dirlist( $path = '.', $include_hidden = true, $recursive = false ) { + + if ( empty( $path ) || '.' == $path ) + $path = $this->cwd(); + + if ( ! $this->exists( $path ) ) + return false; + + $limit_file = false; + if ( $this->is_file( $path ) ) { + $limit_file = $this->locate_node( $path )->name; + $path = dirname( $path ) . '/'; + } + + $ret = array(); + foreach ( $this->fs_map[ $path ]->children as $entry ) { + if ( '.' == $entry->name || '..' == $entry->name ) + continue; + + if ( ! $include_hidden && '.' == $entry->name ) + continue; + + if ( $limit_file && $entry->name != $limit_file ) + continue; + + $struc = array(); + $struc['name'] = $entry->name; + $struc['type'] = $entry->type; + + if ( 'd' == $struc['type'] ) { + if ( $recursive ) + $struc['files'] = $this->dirlist( trailingslashit( $path ) . trailingslashit( $struc['name'] ), $include_hidden, $recursive ); + else + $struc['files'] = array(); + } + + $ret[ $entry->name ] = $struc; + } + return $ret; + } + +} + +class MockFS_Node { + public $name; // The "name" of the entry, does not include a slash (exception, root) + public $type; // The type of the entry 'f' for file, 'd' for Directory + public $path; // The full path to the entry. + + function __construct( $path ) { + $this->path = $path; + $this->name = basename( $path ); + } + + function is_file() { + return $this->type == 'f'; + } + + function is_dir() { + return $this->type == 'd'; + } +} + +class MockFS_Directory_Node extends MockFS_Node { + public $type = 'd'; + public $children = array(); // The child nodes of this directory +} + +class MockFS_File_Node extends MockFS_Node { + public $type = 'f'; + public $contents = ''; // The contents of the file + + function __construct( $path, $contents = '' ) { + parent::__construct( $path ); + $this->contents = $contents; + } +}
\ No newline at end of file diff --git a/tests/phpunit/includes/mock-image-editor.php b/tests/phpunit/includes/mock-image-editor.php new file mode 100644 index 0000000000..2d8164e5c9 --- /dev/null +++ b/tests/phpunit/includes/mock-image-editor.php @@ -0,0 +1,43 @@ +<?php + +if (class_exists( 'WP_Image_Editor' ) ) : + + class WP_Image_Editor_Mock extends WP_Image_Editor { + + public static $load_return = true; + public static $test_return = true; + public static $save_return = array(); + + public function load() { + return self::$load_return; + } + public static function test() { + return self::$test_return; + } + public static function supports_mime_type( $mime_type ) { + return true; + } + public function resize( $max_w, $max_h, $crop = false ) { + + } + public function multi_resize( $sizes ) { + + } + public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { + + } + public function rotate( $angle ) { + + } + public function flip( $horz, $vert ) { + + } + public function save( $destfilename = null, $mime_type = null ) { + return self::$save_return; + } + public function stream( $mime_type = null ) { + + } + } + +endif; diff --git a/tests/phpunit/includes/mock-mailer.php b/tests/phpunit/includes/mock-mailer.php new file mode 100644 index 0000000000..f52a95a064 --- /dev/null +++ b/tests/phpunit/includes/mock-mailer.php @@ -0,0 +1,26 @@ +<?php +require_once( ABSPATH . '/wp-includes/class-phpmailer.php' ); + +class MockPHPMailer extends PHPMailer { + var $mock_sent = array(); + + // override the Send function so it doesn't actually send anything + function Send() { + try { + if ( ! $this->PreSend() ) + return false; + + $this->mock_sent[] = array( + 'to' => $this->to, + 'cc' => $this->cc, + 'bcc' => $this->bcc, + 'header' => $this->MIMEHeader, + 'body' => $this->MIMEBody, + ); + + return true; + } catch ( phpmailerException $e ) { + return false; + } + } +} diff --git a/tests/phpunit/includes/testcase-ajax.php b/tests/phpunit/includes/testcase-ajax.php new file mode 100644 index 0000000000..fcceacc42b --- /dev/null +++ b/tests/phpunit/includes/testcase-ajax.php @@ -0,0 +1,182 @@ +<?php +/** + * Ajax test cases + * + * @package WordPress + * @subpackage UnitTests + * @since 3.4.0 + */ + +/** + * Ajax test case class + * + * @package WordPress + * @subpackage UnitTests + * @since 3.4.0 + */ +abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { + + /** + * Last AJAX response. This is set via echo -or- wp_die. + * @var type + */ + protected $_last_response = ''; + + /** + * List of ajax actions called via POST + * @var type + */ + protected $_core_actions_get = array( 'fetch-list', 'ajax-tag-search', 'wp-compression-test', 'imgedit-preview', 'oembed_cache' ); + + /** + * Saved error reporting level + * @var int + */ + protected $_error_level = 0; + + /** + * List of ajax actions called via GET + * @var type + */ + protected $_core_actions_post = array( + 'oembed_cache', 'image-editor', 'delete-comment', 'delete-tag', 'delete-link', + 'delete-meta', 'delete-post', 'trash-post', 'untrash-post', 'delete-page', 'dim-comment', + 'add-link-category', 'add-tag', 'get-tagcloud', 'get-comments', 'replyto-comment', + 'edit-comment', 'add-menu-item', 'add-meta', 'add-user', 'autosave', 'closed-postboxes', + 'hidden-columns', 'update-welcome-panel', 'menu-get-metabox', 'wp-link-ajax', + 'menu-locations-save', 'menu-quick-search', 'meta-box-order', 'get-permalink', + 'sample-permalink', 'inline-save', 'inline-save-tax', 'find_posts', 'widgets-order', + 'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post', + 'wp-remove-post-lock', 'dismiss-wp-pointer', 'nopriv_autosave' + ); + + /** + * Set up the test fixture. + * Override wp_die(), pretend to be ajax, and suppres E_WARNINGs + */ + public function setUp() { + parent::setUp(); + + // Register the core actions + foreach ( array_merge( $this->_core_actions_get, $this->_core_actions_post ) as $action ) + if ( function_exists( 'wp_ajax_' . str_replace( '-', '_', $action ) ) ) + add_action( 'wp_ajax_' . $action, 'wp_ajax_' . str_replace( '-', '_', $action ), 1 ); + + add_filter( 'wp_die_ajax_handler', array( $this, 'getDieHandler' ), 1, 1 ); + if ( !defined( 'DOING_AJAX' ) ) + define( 'DOING_AJAX', true ); + set_current_screen( 'ajax' ); + + // Clear logout cookies + add_action( 'clear_auth_cookie', array( $this, 'logout' ) ); + + // Suppress warnings from "Cannot modify header information - headers already sent by" + $this->_error_level = error_reporting(); + error_reporting( $this->_error_level & ~E_WARNING ); + + // Make some posts + $this->factory->post->create_many( 5 ); + } + + /** + * Tear down the test fixture. + * Reset $_POST, remove the wp_die() override, restore error reporting + */ + public function tearDown() { + parent::tearDown(); + $_POST = array(); + $_GET = array(); + unset( $GLOBALS['post'] ); + unset( $GLOBALS['comment'] ); + remove_filter( 'wp_die_ajax_handler', array( $this, 'getDieHandler' ), 1, 1 ); + remove_action( 'clear_auth_cookie', array( $this, 'logout' ) ); + error_reporting( $this->_error_level ); + set_current_screen( 'front' ); + } + + /** + * Clear login cookies, unset the current user + */ + public function logout() { + unset( $GLOBALS['current_user'] ); + $cookies = array(AUTH_COOKIE, SECURE_AUTH_COOKIE, LOGGED_IN_COOKIE, USER_COOKIE, PASS_COOKIE); + foreach ( $cookies as $c ) + unset( $_COOKIE[$c] ); + } + + /** + * Return our callback handler + * @return callback + */ + public function getDieHandler() { + return array( $this, 'dieHandler' ); + } + + /** + * Handler for wp_die() + * Save the output for analysis, stop execution by throwing an exception. + * Error conditions (no output, just die) will throw <code>WPAjaxDieStopException( $message )</code> + * You can test for this with: + * <code> + * $this->setExpectedException( 'WPAjaxDieStopException', 'something contained in $message' ); + * </code> + * Normal program termination (wp_die called at then end of output) will throw <code>WPAjaxDieContinueException( $message )</code> + * You can test for this with: + * <code> + * $this->setExpectedException( 'WPAjaxDieContinueException', 'something contained in $message' ); + * </code> + * @param string $message + */ + public function dieHandler( $message ) { + $this->_last_response .= ob_get_clean(); + ob_end_clean(); + if ( '' === $this->_last_response ) { + if ( is_scalar( $message) ) { + throw new WPAjaxDieStopException( (string) $message ); + } else { + throw new WPAjaxDieStopException( '0' ); + } + } else { + throw new WPAjaxDieContinueException( $message ); + } + } + + /** + * Switch between user roles + * E.g. administrator, editor, author, contributor, subscriber + * @param string $role + */ + protected function _setRole( $role ) { + $post = $_POST; + $user_id = $this->factory->user->create( array( 'role' => $role ) ); + wp_set_current_user( $user_id ); + $_POST = array_merge($_POST, $post); + } + + /** + * Mimic the ajax handling of admin-ajax.php + * Capture the output via output buffering, and if there is any, store + * it in $this->_last_message. + * @param string $action + */ + protected function _handleAjax($action) { + + // Start output buffering + ini_set( 'implicit_flush', false ); + ob_start(); + + // Build the request + $_POST['action'] = $action; + $_GET['action'] = $action; + $_REQUEST = array_merge( $_POST, $_GET ); + + // Call the hooks + do_action( 'admin_init' ); + do_action( 'wp_ajax_' . $_REQUEST['action'], null ); + + // Save the output + $buffer = ob_get_clean(); + if ( !empty( $buffer ) ) + $this->_last_response = $buffer; + } +} diff --git a/tests/phpunit/includes/testcase-xmlrpc.php b/tests/phpunit/includes/testcase-xmlrpc.php new file mode 100644 index 0000000000..e82c5e134e --- /dev/null +++ b/tests/phpunit/includes/testcase-xmlrpc.php @@ -0,0 +1,30 @@ +<?php +include_once(ABSPATH . 'wp-admin/includes/admin.php'); +include_once(ABSPATH . WPINC . '/class-IXR.php'); +include_once(ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'); + +class WP_XMLRPC_UnitTestCase extends WP_UnitTestCase { + protected $myxmlrpcserver; + + function setUp() { + parent::setUp(); + + add_filter( 'pre_option_enable_xmlrpc', '__return_true' ); + + $this->myxmlrpcserver = new wp_xmlrpc_server(); + } + + function tearDown() { + remove_filter( 'pre_option_enable_xmlrpc', '__return_true' ); + + parent::tearDown(); + } + + protected function make_user_by_role( $role ) { + return $this->factory->user->create( array( + 'user_login' => $role, + 'user_pass' => $role, + 'role' => $role + )); + } +} diff --git a/tests/phpunit/includes/testcase.php b/tests/phpunit/includes/testcase.php new file mode 100644 index 0000000000..c3fb4373df --- /dev/null +++ b/tests/phpunit/includes/testcase.php @@ -0,0 +1,227 @@ +<?php + +require_once dirname( __FILE__ ) . '/factory.php'; +require_once dirname( __FILE__ ) . '/trac.php'; + +class WP_UnitTestCase extends PHPUnit_Framework_TestCase { + + protected static $forced_tickets = array(); + + /** + * @var WP_UnitTest_Factory + */ + protected $factory; + + function setUp() { + set_time_limit(0); + + global $wpdb; + $wpdb->suppress_errors = false; + $wpdb->show_errors = true; + $wpdb->db_connect(); + ini_set('display_errors', 1 ); + $this->factory = new WP_UnitTest_Factory; + $this->clean_up_global_scope(); + $this->start_transaction(); + add_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) ); + } + + function tearDown() { + global $wpdb; + $wpdb->query( 'ROLLBACK' ); + remove_filter( 'dbdelta_create_queries', array( $this, '_create_temporary_tables' ) ); + remove_filter( 'query', array( $this, '_drop_temporary_tables' ) ); + remove_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) ); + } + + function clean_up_global_scope() { + $_GET = array(); + $_POST = array(); + $this->flush_cache(); + } + + function flush_cache() { + global $wp_object_cache; + $wp_object_cache->group_ops = array(); + $wp_object_cache->stats = array(); + $wp_object_cache->memcache_debug = array(); + $wp_object_cache->cache = array(); + if ( method_exists( $wp_object_cache, '__remoteset' ) ) { + $wp_object_cache->__remoteset(); + } + wp_cache_flush(); + wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache' ) ); + wp_cache_add_non_persistent_groups( array( 'comment', 'counts', 'plugins' ) ); + } + + function start_transaction() { + global $wpdb; + $wpdb->query( 'SET autocommit = 0;' ); + $wpdb->query( 'START TRANSACTION;' ); + add_filter( 'dbdelta_create_queries', array( $this, '_create_temporary_tables' ) ); + add_filter( 'query', array( $this, '_drop_temporary_tables' ) ); + } + + function _create_temporary_tables( $queries ) { + return str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $queries ); + } + + function _drop_temporary_tables( $query ) { + if ( 'DROP TABLE' === substr( $query, 0, 10 ) ) + return 'DROP TEMPORARY TABLE ' . substr( $query, 10 ); + return $query; + } + + function get_wp_die_handler( $handler ) { + return array( $this, 'wp_die_handler' ); + } + + function wp_die_handler( $message ) { + throw new WPDieException( $message ); + } + + function assertWPError( $actual, $message = '' ) { + $this->assertInstanceOf( 'WP_Error', $actual, $message ); + } + + function assertEqualFields( $object, $fields ) { + foreach( $fields as $field_name => $field_value ) { + if ( $object->$field_name != $field_value ) { + $this->fail(); + } + } + } + + function assertDiscardWhitespace( $expected, $actual ) { + $this->assertEquals( preg_replace( '/\s*/', '', $expected ), preg_replace( '/\s*/', '', $actual ) ); + } + + function assertEqualSets( $expected, $actual ) { + $this->assertEquals( array(), array_diff( $expected, $actual ) ); + $this->assertEquals( array(), array_diff( $actual, $expected ) ); + } + + function go_to( $url ) { + // note: the WP and WP_Query classes like to silently fetch parameters + // from all over the place (globals, GET, etc), which makes it tricky + // to run them more than once without very carefully clearing everything + $_GET = $_POST = array(); + foreach (array('query_string', 'id', 'postdata', 'authordata', 'day', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages', 'pagenow') as $v) { + if ( isset( $GLOBALS[$v] ) ) unset( $GLOBALS[$v] ); + } + $parts = parse_url($url); + if (isset($parts['scheme'])) { + $req = $parts['path']; + if (isset($parts['query'])) { + $req .= '?' . $parts['query']; + // parse the url query vars into $_GET + parse_str($parts['query'], $_GET); + } + } else { + $req = $url; + } + if ( ! isset( $parts['query'] ) ) { + $parts['query'] = ''; + } + + $_SERVER['REQUEST_URI'] = $req; + unset($_SERVER['PATH_INFO']); + + $this->flush_cache(); + unset($GLOBALS['wp_query'], $GLOBALS['wp_the_query']); + $GLOBALS['wp_the_query'] = new WP_Query(); + $GLOBALS['wp_query'] = $GLOBALS['wp_the_query']; + $GLOBALS['wp'] = new WP(); + + // clean out globals to stop them polluting wp and wp_query + foreach ($GLOBALS['wp']->public_query_vars as $v) { + unset($GLOBALS[$v]); + } + foreach ($GLOBALS['wp']->private_query_vars as $v) { + unset($GLOBALS[$v]); + } + + $GLOBALS['wp']->main($parts['query']); + } + + protected function checkRequirements() { + parent::checkRequirements(); + if ( WP_TESTS_FORCE_KNOWN_BUGS ) + return; + $tickets = PHPUnit_Util_Test::getTickets( get_class( $this ), $this->getName( false ) ); + foreach ( $tickets as $ticket ) { + if ( is_numeric( $ticket ) ) { + $this->knownWPBug( $ticket ); + } elseif ( 'UT' == substr( $ticket, 0, 2 ) ) { + $ticket = substr( $ticket, 2 ); + if ( $ticket && is_numeric( $ticket ) ) + $this->knownUTBug( $ticket ); + } elseif ( 'Plugin' == substr( $ticket, 0, 6 ) ) { + $ticket = substr( $ticket, 6 ); + if ( $ticket && is_numeric( $ticket ) ) + $this->knownPluginBug( $ticket ); + } + } + } + + /** + * Skips the current test if there is an open WordPress ticket with id $ticket_id + */ + function knownWPBug( $ticket_id ) { + if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( $ticket_id, self::$forced_tickets ) ) + return; + if ( ! TracTickets::isTracTicketClosed( 'http://core.trac.wordpress.org', $ticket_id ) ) + $this->markTestSkipped( sprintf( 'WordPress Ticket #%d is not fixed', $ticket_id ) ); + } + + /** + * Skips the current test if there is an open unit tests ticket with id $ticket_id + */ + function knownUTBug( $ticket_id ) { + if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( 'UT' . $ticket_id, self::$forced_tickets ) ) + return; + if ( ! TracTickets::isTracTicketClosed( 'http://unit-tests.trac.wordpress.org', $ticket_id ) ) + $this->markTestSkipped( sprintf( 'Unit Tests Ticket #%d is not fixed', $ticket_id ) ); + } + + /** + * Skips the current test if there is an open plugin ticket with id $ticket_id + */ + function knownPluginBug( $ticket_id ) { + if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( 'Plugin' . $ticket_id, self::$forced_tickets ) ) + return; + if ( ! TracTickets::isTracTicketClosed( 'http://plugins.trac.wordpress.org', $ticket_id ) ) + $this->markTestSkipped( sprintf( 'WordPress Plugin Ticket #%d is not fixed', $ticket_id ) ); + } + + public static function forceTicket( $ticket ) { + self::$forced_tickets[] = $ticket; + } + + /** + * Define constants after including files. + */ + function prepareTemplate( Text_Template $template ) { + $template->setVar( array( 'constants' => '' ) ); + $template->setVar( array( 'wp_constants' => PHPUnit_Util_GlobalState::getConstantsAsString() ) ); + parent::prepareTemplate( $template ); + } + + /** + * Returns the name of a temporary file + */ + function temp_filename() { + $tmp_dir = ''; + $dirs = array( 'TMP', 'TMPDIR', 'TEMP' ); + foreach( $dirs as $dir ) + if ( isset( $_ENV[$dir] ) && !empty( $_ENV[$dir] ) ) { + $tmp_dir = $dir; + break; + } + if ( empty( $tmp_dir ) ) { + $tmp_dir = '/tmp'; + } + $tmp_dir = realpath( $dir ); + return tempnam( $tmp_dir, 'wpunit' ); + } +} diff --git a/tests/phpunit/includes/trac.php b/tests/phpunit/includes/trac.php new file mode 100644 index 0000000000..70b56a0a13 --- /dev/null +++ b/tests/phpunit/includes/trac.php @@ -0,0 +1,54 @@ +<?php + +class TracTickets { + /** + * When open tickets for a Trac install is requested, the results are stored here. + * + * @var array + */ + protected static $trac_ticket_cache = array(); + + /** + * Checks if track ticket #$ticket_id is resolved + * + * @return bool|null true if the ticket is resolved, false if not resolved, null on error + */ + public static function isTracTicketClosed( $trac_url, $ticket_id ) { + if ( ! isset( self::$trac_ticket_cache[ $trac_url ] ) ) { + // In case you're running the tests offline, keep track of open tickets. + $file = DIR_TESTDATA . '/.trac-ticket-cache.' . str_replace( array( 'http://', 'https://', '/' ), array( '', '', '-' ), rtrim( $trac_url, '/' ) ); + $tickets = @file_get_contents( $trac_url . '/query?status=%21closed&format=csv&col=id' ); + // Check if our HTTP request failed. + if ( false === $tickets ) { + if ( file_exists( $file ) ) { + register_shutdown_function( array( 'TracTickets', 'usingLocalCache' ) ); + $tickets = file_get_contents( $file ); + } else { + register_shutdown_function( array( 'TracTickets', 'forcingKnownBugs' ) ); + self::$trac_ticket_cache[ $trac_url ] = array(); + return true; // Assume the ticket is closed, which means it gets run. + } + } else { + $tickets = substr( $tickets, 2 ); // remove 'id' column header + $tickets = trim( $tickets ); + file_put_contents( $file, $tickets ); + } + $tickets = explode( "\r\n", $tickets ); + self::$trac_ticket_cache[ $trac_url ] = $tickets; + } + + return ! in_array( $ticket_id, self::$trac_ticket_cache[ $trac_url ] ); + } + + public static function usingLocalCache() { + echo PHP_EOL . "\x1b[0m\x1b[30;43m\x1b[2K"; + echo 'INFO: Trac was inaccessible, so a local ticket status cache was used.' . PHP_EOL; + echo "\x1b[0m\x1b[2K"; + } + + public static function forcingKnownBugs() { + echo PHP_EOL . "\x1b[0m\x1b[37;41m\x1b[2K"; + echo "ERROR: Trac was inaccessible, so known bugs weren't able to be skipped." . PHP_EOL; + echo "\x1b[0m\x1b[2K"; + } +} diff --git a/tests/phpunit/includes/utils.php b/tests/phpunit/includes/utils.php new file mode 100644 index 0000000000..b6d722663c --- /dev/null +++ b/tests/phpunit/includes/utils.php @@ -0,0 +1,365 @@ +<?php + +// misc help functions and utilities + +function rand_str($len=32) { + return substr(md5(uniqid(rand())), 0, $len); +} + +// strip leading and trailing whitespace from each line in the string +function strip_ws($txt) { + $lines = explode("\n", $txt); + $result = array(); + foreach ($lines as $line) + if (trim($line)) + $result[] = trim($line); + + return trim(join("\n", $result)); +} + +// helper class for testing code that involves actions and filters +// typical use: +// $ma = new MockAction(); +// add_action('foo', array(&$ma, 'action')); +class MockAction { + var $events; + var $debug; + + function MockAction($debug=0) { + $this->reset(); + $this->debug = $debug; + } + + function reset() { + $this->events = array(); + } + + function current_filter() { + if (is_callable('current_filter')) + return current_filter(); + global $wp_actions; + return end($wp_actions); + } + + function action($arg) { +if ($this->debug) dmp(__FUNCTION__, $this->current_filter()); + $args = func_get_args(); + $this->events[] = array('action' => __FUNCTION__, 'tag'=>$this->current_filter(), 'args'=>$args); + return $arg; + } + + function action2($arg) { +if ($this->debug) dmp(__FUNCTION__, $this->current_filter()); + + $args = func_get_args(); + $this->events[] = array('action' => __FUNCTION__, 'tag'=>$this->current_filter(), 'args'=>$args); + return $arg; + } + + function filter($arg) { +if ($this->debug) dmp(__FUNCTION__, $this->current_filter()); + + $args = func_get_args(); + $this->events[] = array('filter' => __FUNCTION__, 'tag'=>$this->current_filter(), 'args'=>$args); + return $arg; + } + + function filter2($arg) { +if ($this->debug) dmp(__FUNCTION__, $this->current_filter()); + + $args = func_get_args(); + $this->events[] = array('filter' => __FUNCTION__, 'tag'=>$this->current_filter(), 'args'=>$args); + return $arg; + } + + function filter_append($arg) { +if ($this->debug) dmp(__FUNCTION__, $this->current_filter()); + + $args = func_get_args(); + $this->events[] = array('filter' => __FUNCTION__, 'tag'=>$this->current_filter(), 'args'=>$args); + return $arg . '_append'; + } + + function filterall($tag, $arg=NULL) { + // this one doesn't return the result, so it's safe to use with the new 'all' filter +if ($this->debug) dmp(__FUNCTION__, $this->current_filter()); + + $args = func_get_args(); + $this->events[] = array('filter' => __FUNCTION__, 'tag'=>$tag, 'args'=>array_slice($args, 1)); + } + + // return a list of all the actions, tags and args + function get_events() { + return $this->events; + } + + // return a count of the number of times the action was called since the last reset + function get_call_count($tag='') { + if ($tag) { + $count = 0; + foreach ($this->events as $e) + if ($e['action'] == $tag) + ++$count; + return $count; + } + return count($this->events); + } + + // return an array of the tags that triggered calls to this action + function get_tags() { + $out = array(); + foreach ($this->events as $e) { + $out[] = $e['tag']; + } + return $out; + } + + // return an array of args passed in calls to this action + function get_args() { + $out = array(); + foreach ($this->events as $e) + $out[] = $e['args']; + return $out; + } +} + +// convert valid xml to an array tree structure +// kinda lame but it works with a default php 4 install +class testXMLParser { + var $xml; + var $data = array(); + + function testXMLParser($in) { + $this->xml = xml_parser_create(); + xml_set_object($this->xml, $this); + xml_parser_set_option($this->xml,XML_OPTION_CASE_FOLDING, 0); + xml_set_element_handler($this->xml, array(&$this, 'startHandler'), array(&$this, 'endHandler')); + xml_set_character_data_handler($this->xml, array(&$this, 'dataHandler')); + $this->parse($in); + } + + function parse($in) { + $parse = xml_parse($this->xml, $in, sizeof($in)); + if (!$parse) { + trigger_error(sprintf("XML error: %s at line %d", + xml_error_string(xml_get_error_code($this->xml)), + xml_get_current_line_number($this->xml)), E_USER_ERROR); + xml_parser_free($this->xml); + } + return true; + } + + function startHandler($parser, $name, $attributes) { + $data['name'] = $name; + if ($attributes) { $data['attributes'] = $attributes; } + $this->data[] = $data; + } + + function dataHandler($parser, $data) { + $index = count($this->data) - 1; + @$this->data[$index]['content'] .= $data; + } + + function endHandler($parser, $name) { + if (count($this->data) > 1) { + $data = array_pop($this->data); + $index = count($this->data) - 1; + $this->data[$index]['child'][] = $data; + } + } +} + +function xml_to_array($in) { + $p = new testXMLParser($in); + return $p->data; +} + +function xml_find($tree /*, $el1, $el2, $el3, .. */) { + $a = func_get_args(); + $a = array_slice($a, 1); + $n = count($a); + $out = array(); + + if ($n < 1) + return $out; + + for ($i=0; $i<count($tree); $i++) { +# echo "checking '{$tree[$i][name]}' == '{$a[0]}'\n"; +# var_dump($tree[$i]['name'], $a[0]); + if ($tree[$i]['name'] == $a[0]) { +# echo "n == {$n}\n"; + if ($n == 1) + $out[] = $tree[$i]; + else { + $subtree =& $tree[$i]['child']; + $call_args = array($subtree); + $call_args = array_merge($call_args, array_slice($a, 1)); + $out = array_merge($out, call_user_func_array('xml_find', $call_args)); + } + } + } + + return $out; +} + +function xml_join_atts($atts) { + $a = array(); + foreach ($atts as $k=>$v) + $a[] = $k.'="'.$v.'"'; + return join(' ', $a); +} + +function xml_array_dumbdown(&$data) { + $out = array(); + + foreach (array_keys($data) as $i) { + $name = $data[$i]['name']; + if (!empty($data[$i]['attributes'])) + $name .= ' '.xml_join_atts($data[$i]['attributes']); + + if (!empty($data[$i]['child'])) { + $out[$name][] = xml_array_dumbdown($data[$i]['child']); + } + else + $out[$name] = $data[$i]['content']; + } + + return $out; +} + +function dmp() { + $args = func_get_args(); + + foreach ($args as $thing) + echo (is_scalar($thing) ? strval($thing) : var_export($thing, true)), "\n"; +} + +function dmp_filter($a) { + dmp($a); + return $a; +} + +function get_echo($callable, $args = array()) { + ob_start(); + call_user_func_array($callable, $args); + return ob_get_clean(); +} + +// recursively generate some quick assertEquals tests based on an array +function gen_tests_array($name, $array) { + $out = array(); + foreach ($array as $k=>$v) { + if (is_numeric($k)) + $index = strval($k); + else + $index = "'".addcslashes($k, "\n\r\t'\\")."'"; + + if (is_string($v)) { + $out[] = '$this->assertEquals( \'' . addcslashes($v, "\n\r\t'\\") . '\', $'.$name.'['.$index.'] );'; + } + elseif (is_numeric($v)) { + $out[] = '$this->assertEquals( ' . $v . ', $'.$name.'['.$index.'] );'; + } + elseif (is_array($v)) { + $out[] = gen_tests_array("{$name}[{$index}]", $v); + } + } + return join("\n", $out)."\n"; +} + +/** + * Use to create objects by yourself + */ +class MockClass {}; + +/** + * Drops all tables from the WordPress database + */ +function drop_tables() { + global $wpdb; + $tables = $wpdb->get_col('SHOW TABLES;'); + foreach ($tables as $table) + $wpdb->query("DROP TABLE IF EXISTS {$table}"); +} + +function print_backtrace() { + $bt = debug_backtrace(); + echo "Backtrace:\n"; + $i = 0; + foreach ($bt as $stack) { + echo ++$i, ": "; + if ( isset($stack['class']) ) + echo $stack['class'].'::'; + if ( isset($stack['function']) ) + echo $stack['function'].'() '; + echo "line {$stack[line]} in {$stack[file]}\n"; + } + echo "\n"; +} + +// mask out any input fields matching the given name +function mask_input_value($in, $name='_wpnonce') { + return preg_replace('@<input([^>]*) name="'.preg_quote($name).'"([^>]*) value="[^>]*" />@', '<input$1 name="'.preg_quote($name).'"$2 value="***" />', $in); +} + +$GLOBALS['_wp_die_disabled'] = false; +function _wp_die_handler( $message, $title = '', $args = array() ) { + if ( !$GLOBALS['_wp_die_disabled'] ) { + _default_wp_die_handler( $message, $title, $args ); + } else { + //Ignore at our peril + } +} + +function _disable_wp_die() { + $GLOBALS['_wp_die_disabled'] = true; +} + +function _enable_wp_die() { + $GLOBALS['_wp_die_disabled'] = false; +} + +function _wp_die_handler_filter() { + return '_wp_die_handler'; +} + +if ( !function_exists( 'str_getcsv' ) ) { + function str_getcsv( $input, $delimiter = ',', $enclosure = '"', $escape = "\\" ) { + $fp = fopen( 'php://temp/', 'r+' ); + fputs( $fp, $input ); + rewind( $fp ); + $data = fgetcsv( $fp, strlen( $input ), $delimiter, $enclosure ); + fclose( $fp ); + return $data; + } +} + +function _rmdir( $path ) { + if ( in_array(basename( $path ), array( '.', '..' ) ) ) { + return; + } elseif ( is_file( $path ) ) { + unlink( $path ); + } elseif ( is_dir( $path ) ) { + foreach ( scandir( $path ) as $file ) + _rmdir( $path . '/' . $file ); + rmdir( $path ); + } +} + +/** + * Removes the post type and its taxonomy associations. + */ +function _unregister_post_type( $cpt_name ) { + unset( $GLOBALS['wp_post_types'][ $cpt_name ] ); + unset( $GLOBALS['_wp_post_type_features'][ $cpt_name ] ); + + foreach ( $GLOBALS['wp_taxonomies'] as $taxonomy ) { + if ( false !== $key = array_search( $cpt_name, $taxonomy->object_type ) ) { + unset( $taxonomy->object_type[$key] ); + } + } +} + +function _unregister_taxonomy( $taxonomy_name ) { + unset( $GLOBALS['wp_taxonomies'][$taxonomy_name] ); +} diff --git a/tests/phpunit/includes/wp-profiler.php b/tests/phpunit/includes/wp-profiler.php new file mode 100644 index 0000000000..1ea4d2e49e --- /dev/null +++ b/tests/phpunit/includes/wp-profiler.php @@ -0,0 +1,216 @@ +<?php + +/* +A simple manually-instrumented profiler for WordPress. + +This records basic execution time, and a summary of the actions and SQL queries run within each block. + +start() and stop() must be called in pairs, for example: + +function something_to_profile() { + wppf_start(__FUNCTION__); + do_stuff(); + wppf_stop(); +} + +Multiple profile blocks are permitted, and they may be nested. + +*/ + +class WPProfiler { + var $stack; + var $profile; + + // constructor + function WPProfiler() { + $this->stack = array(); + $this->profile = array(); + } + + function start($name) { + $time = $this->microtime(); + + if (!$this->stack) { + // log all actions and filters + add_filter('all', array(&$this, 'log_filter')); + } + + // reset the wpdb queries log, storing it on the profile stack if necessary + global $wpdb; + if ($this->stack) { + $this->stack[count($this->stack)-1]['queries'] = $wpdb->queries; + } + $wpdb->queries = array(); + + global $wp_object_cache; + + $this->stack[] = array( + 'start' => $time, + 'name' => $name, + 'cache_cold_hits' => $wp_object_cache->cold_cache_hits, + 'cache_warm_hits' => $wp_object_cache->warm_cache_hits, + 'cache_misses' => $wp_object_cache->cache_misses, + 'cache_dirty_objects' => $this->_dirty_objects_count($wp_object_cache->dirty_objects), + 'actions' => array(), + 'filters' => array(), + 'queries' => array(), + ); + + } + + function stop() { + $item = array_pop($this->stack); + $time = $this->microtime($item['start']); + $name = $item['name']; + + global $wpdb; + $item['queries'] = $wpdb->queries; + global $wp_object_cache; + + $cache_dirty_count = $this->_dirty_objects_count($wp_object_cache->dirty_objects); + $cache_dirty_delta = $this->array_sub($cache_dirty_count, $item['cache_dirty_objects']); + + if (isset($this->profile[$name])) { + $this->profile[$name]['time'] += $time; + $this->profile[$name]['calls'] ++; + $this->profile[$name]['cache_cold_hits'] += ($wp_object_cache->cold_cache_hits - $item['cache_cold_hits']); + $this->profile[$name]['cache_warm_hits'] += ($wp_object_cache->warm_cache_hits - $item['cache_warm_hits']); + $this->profile[$name]['cache_misses'] += ($wp_object_cache->cache_misses - $item['cache_misses']); + $this->profile[$name]['cache_dirty_objects'] = array_add( $this->profile[$name]['cache_dirty_objects'], $cache_dirty_delta) ; + $this->profile[$name]['actions'] = array_add( $this->profile[$name]['actions'], $item['actions'] ); + $this->profile[$name]['filters'] = array_add( $this->profile[$name]['filters'], $item['filters'] ); + $this->profile[$name]['queries'] = array_add( $this->profile[$name]['queries'], $item['queries'] ); + #$this->_query_summary($item['queries'], $this->profile[$name]['queries']); + + } + else { + $queries = array(); + $this->_query_summary($item['queries'], $queries); + $this->profile[$name] = array( + 'time' => $time, + 'calls' => 1, + 'cache_cold_hits' => ($wp_object_cache->cold_cache_hits - $item['cache_cold_hits']), + 'cache_warm_hits' => ($wp_object_cache->warm_cache_hits - $item['cache_warm_hits']), + 'cache_misses' => ($wp_object_cache->cache_misses - $item['cache_misses']), + 'cache_dirty_objects' => $cache_dirty_delta, + 'actions' => $item['actions'], + 'filters' => $item['filters'], +# 'queries' => $item['queries'], + 'queries' => $queries, + ); + } + + if (!$this->stack) { + remove_filter('all', array(&$this, 'log_filter')); + } + } + + function microtime($since = 0.0) { + list($usec, $sec) = explode(' ', microtime()); + return (float)$sec + (float)$usec - $since; + } + + function log_filter($tag) { + if ($this->stack) { + global $wp_actions; + if ($tag == end($wp_actions)) + @$this->stack[count($this->stack)-1]['actions'][$tag] ++; + else + @$this->stack[count($this->stack)-1]['filters'][$tag] ++; + } + return $arg; + } + + function log_action($tag) { + if ($this->stack) + @$this->stack[count($this->stack)-1]['actions'][$tag] ++; + } + + function _current_action() { + global $wp_actions; + return $wp_actions[count($wp_actions)-1]; + } + + function results() { + return $this->profile; + } + + function _query_summary($queries, &$out) { + foreach ($queries as $q) { + $sql = $q[0]; + $sql = preg_replace('/(WHERE \w+ =) \d+/', '$1 x', $sql); + $sql = preg_replace('/(WHERE \w+ =) \'\[-\w]+\'/', '$1 \'xxx\'', $sql); + + @$out[$sql] ++; + } + asort($out); + return; + } + + function _query_count($queries) { + // this requires the savequeries patch at http://trac.wordpress.org/ticket/5218 + $out = array(); + foreach ($queries as $q) { + if (empty($q[2])) + @$out['unknown'] ++; + else + @$out[$q[2]] ++; + } + return $out; + } + + function _dirty_objects_count($dirty_objects) { + $out = array(); + foreach (array_keys($dirty_objects) as $group) + $out[$group] = count($dirty_objects[$group]); + return $out; + } + + function array_add($a, $b) { + $out = $a; + foreach (array_keys($b) as $key) + if (array_key_exists($key, $out)) + $out[$key] += $b[$key]; + else + $out[$key] = $b[$key]; + return $out; + } + + function array_sub($a, $b) { + $out = $a; + foreach (array_keys($b) as $key) + if (array_key_exists($key, $b)) + $out[$key] -= $b[$key]; + return $out; + } + + function print_summary() { + $results = $this->results(); + + printf("\nname calls time action filter warm cold misses dirty\n"); + foreach ($results as $name=>$stats) { + printf("%24.24s %6d %6.4f %6d %6d %6d %6d %6d %6d\n", $name, $stats['calls'], $stats['time'], array_sum($stats['actions']), array_sum($stats['filters']), $stats['cache_warm_hits'], $stats['cache_cold_hits'], $stats['cache_misses'], array_sum($stats['cache_dirty_objects'])); + } + } +} + +global $wppf; +$wppf = new WPProfiler(); + +function wppf_start($name) { + $GLOBALS['wppf']->start($name); +} + +function wppf_stop() { + $GLOBALS['wppf']->stop(); +} + +function wppf_results() { + return $GLOBALS['wppf']->results(); +} + +function wppf_print_summary() { + $GLOBALS['wppf']->print_summary(); +} + +?>
\ No newline at end of file |