Miscellaneous code hacking fixes:

- Fix how CodeHackerTestHook::executeBeforeTest parses the test name,
  to account for warnings and tests with data sets.

- CodeHackerTestHook now includes a executeAfterTest hook that
  disables the code hacker (needed to prevent it from inadvertently
  altering further tests). Also, clear_hacks is executed in
  executeBeforeTest for the same reason.

- CodeHacker gets restore, clear_hacks and is_enabled methods
  to support the changes in CodeHackerTestHook.

- FunctionsMockerHack fixed so that it doesn't modify strings
  that are class method definitions.

- Added the WC_Unit_Test_Case::file_copy method, it must be used
  instead of the PHP built-in "copy" in tests, otherwise tests
  that run with the code hacker active will fail.
  This is something to investigate.
This commit is contained in:
Nestor Soriano 2020-04-12 13:09:57 +02:00
parent 9a5b3b353d
commit 1a68abbc28
8 changed files with 100 additions and 17 deletions

View File

@ -1,6 +1,7 @@
<?php
use PHPUnit\Runner\BeforeTestHook;
use PHPUnit\Runner\AfterTestHook;
/**
* Helper to use the CodeHacker class in PHPUnit.
@ -44,19 +45,37 @@ use PHPUnit\Runner\BeforeTestHook;
* Parameters specified after the class name will be passed to the class constructor.
* Hacks defined as class annotations will be applied to all tests.
*/
final class CodeHackerTestHook implements BeforeTestHook {
final class CodeHackerTestHook implements BeforeTestHook, AfterTestHook {
public function __construct() {
include_once __DIR__ . '/code-hacker.php';
}
public function executeAfterTest( string $test, float $time ): void {
CodeHacker::restore();
}
public function executeBeforeTest( string $test ): void {
$parts = explode( '::', $test );
/**
* Possible formats of $test:
* TestClass::TestMethod
* TestClass::TestMethod with data set #...
* Warning
*/
$parts = explode( '::', $test );
if ( count( $parts ) < 2 ) {
return;
}
$class_name = $parts[0];
$method_name = $parts[1];
$method_name = explode( ' ', $parts[1] )[0];
// Make code hacker class and individual hack classes visible
include_once __DIR__ . '/code-hacker.php';
foreach ( glob( __DIR__ . '/hacks/*.php' ) as $hack_class_file ) {
include_once $hack_class_file;
}
CodeHacker::clear_hacks();
$this->execute_before_methods( $class_name, $method_name );
$has_class_annotation_hacks = $this->add_hacks_from_annotations( new ReflectionClass( $class_name ) );

View File

@ -33,9 +33,29 @@ class CodeHacker {
private static $hacks = array();
private static $enabled = false;
public static function enable() {
stream_wrapper_unregister( self::PROTOCOL );
stream_wrapper_register( self::PROTOCOL, __CLASS__ );
if ( ! self::$enabled ) {
stream_wrapper_unregister( self::PROTOCOL );
stream_wrapper_register( self::PROTOCOL, __CLASS__ );
self::$enabled = true;
}
}
public static function restore() {
if ( self::$enabled ) {
stream_wrapper_restore( self::PROTOCOL );
self::$enabled = false;
}
}
public static function clear_hacks() {
self::$hacks = array();
}
public static function is_enabled() {
return self::$enabled;
}
public static function add_hack( $hack ) {
@ -98,6 +118,7 @@ class CodeHacker {
public function stream_close() {
// echo "***** CLOSE HANDLE: " . $this->handle . " \n";
fclose( $this->handle );
}

View File

@ -38,6 +38,16 @@ abstract class CodeHack {
return is_array( $token ) && $type === $token[0];
}
/**
* Return the type of a given token.
*
* @param mixed $token Token to check.
* @return mixed|null Type of token (see https://www.php.net/manual/en/tokens.php), or null if it's a character.
*/
protected function token_type_of( $token ) {
return is_array( $token ) ? $token[0] : null;
}
/**
* Converts a token to its string representation.
*

View File

@ -14,6 +14,13 @@ require_once __DIR__ . '/code-hack.php';
*/
final class FunctionsMockerHack extends CodeHack {
private static $non_global_function_tokens = array(
T_PAAMAYIM_NEKUDOTAYIM,
T_DOUBLE_COLON,
T_OBJECT_OPERATOR,
T_FUNCTION,
);
/**
* FunctionsMockerHack constructor.
*
@ -35,16 +42,19 @@ final class FunctionsMockerHack extends CodeHack {
}
public function hack( $code, $path ) {
$tokens = $this->tokenize( $code );
$code = '';
$previous_token_is_object_operator = false;
$tokens = $this->tokenize( $code );
$code = '';
$previous_token_is_non_global_function_qualifier = false;
foreach ( $tokens as $token ) {
if ( $this->is_token_of_type( $token, T_STRING ) && ! $previous_token_is_object_operator && in_array( $token[1], $this->mocked_methods ) ) {
$token_type = $this->token_type_of( $token );
if ( T_WHITESPACE === $token_type ) {
$code .= $this->token_to_string( $token );
} elseif ( T_STRING === $token_type && ! $previous_token_is_non_global_function_qualifier && in_array( $token[1], $this->mocked_methods ) ) {
$code .= "{$this->mock_class}::{$token[1]}";
} else {
$code .= $this->token_to_string( $token );
$previous_token_is_object_operator = $this->is_token_of_type( $token, T_DOUBLE_COLON ) || $this->is_token_of_type( $token, T_OBJECT_OPERATOR );
$code .= $this->token_to_string( $token );
$previous_token_is_non_global_function_qualifier = in_array( $token_type, self::$non_global_function_tokens );
}
}

View File

@ -99,4 +99,27 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
$message = $message ? $message : "We're all doomed!";
throw new Exception( $message, $code );
}
/**
* Copies a file, temporarily disabling the code hacker.
* Use this instead of "copy" in tests for compatibility with the code hacker.
*
* TODO: Investigate why invoking "copy" within a test with the code hacker active causes the test to fail.
*
* @param string $source Path to the source file.
* @param string $dest The destination path.
* @param resource $context [optional] A valid context resource created with stream_context_create.
* @return bool true on success or false on failure.
*/
public function file_copy( $source, $dest, $context = null ) {
if ( CodeHacker::is_enabled() ) {
CodeHacker::restore();
$result = copy( $source, $dest, $context );
CodeHacker::enable();
return $result;
} else {
return copy( $source, $dest, $context );
}
}
}

View File

@ -147,7 +147,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
* @return void
*/
public function test_server_file() {
copy( $this->csv_file, ABSPATH . '/sample.csv' );
$this->file_copy( $this->csv_file, ABSPATH . '/sample.csv' );
$_POST['file_url'] = 'sample.csv';
$import_controller = new WC_Product_CSV_Importer_Controller();
$this->assertEquals( ABSPATH . 'sample.csv', $import_controller->handle_upload() );
@ -644,7 +644,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
if ( false !== strpos( $url, 'http://demo.woothemes.com' ) ) {
if ( ! empty( $request['filename'] ) ) {
copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png', $request['filename'] );
$this->file_copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png', $request['filename'] );
}
$mocked_response = array(

View File

@ -126,7 +126,7 @@ class WC_Tests_MaxMind_Database extends WC_Unit_Test_Case {
if ( 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=testing_license&suffix=tar.gz' === $url ) {
// We need to copy the file to where the request is supposed to have streamed it.
copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/GeoLite2-Country.tar.gz', $request['filename'] );
$this->file_copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/GeoLite2-Country.tar.gz', $request['filename'] );
$mocked_response = array(
'response' => array( 'code' => 200 ),

View File

@ -236,14 +236,14 @@ class WC_Tests_API_Functions extends WC_Unit_Test_Case {
} elseif ( 'http://somedomain.com/invalid-image-2.png' === $url ) {
// image with an unsupported mime type.
// we need to manually copy the file as we are mocking the request. without this an empty file is created.
copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/file.txt', $request['filename'] );
$this->file_copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/file.txt', $request['filename'] );
$mocked_response = array(
'response' => array( 'code' => 200 ),
);
} elseif ( 'http://somedomain.com/' . $this->file_name === $url ) {
// we need to manually copy the file as we are mocking the request. without this an empty file is created.
copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png', $request['filename'] );
$this->file_copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png', $request['filename'] );
$mocked_response = array(
'response' => array( 'code' => 200 ),