2020-04-09 14:32:40 +00:00
|
|
|
<?php
|
2020-04-16 13:03:15 +00:00
|
|
|
/**
|
|
|
|
* CodeHackerTestHook class file.
|
|
|
|
*
|
|
|
|
* @package WooCommerce/Testing
|
|
|
|
*/
|
|
|
|
|
|
|
|
// phpcs:disable Squiz.Commenting.FunctionComment.Missing, PHPCompatibility.FunctionDeclarations
|
2020-04-09 14:32:40 +00:00
|
|
|
|
2020-04-13 07:32:19 +00:00
|
|
|
namespace Automattic\WooCommerce\Testing\CodeHacking;
|
|
|
|
|
2020-04-09 14:32:40 +00:00
|
|
|
use PHPUnit\Runner\BeforeTestHook;
|
2020-04-12 11:09:57 +00:00
|
|
|
use PHPUnit\Runner\AfterTestHook;
|
2020-04-13 07:32:19 +00:00
|
|
|
use PHPUnit\Util\Test;
|
|
|
|
use ReflectionClass;
|
|
|
|
use ReflectionMethod;
|
|
|
|
use Exception;
|
2020-04-09 14:32:40 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper to use the CodeHacker class in PHPUnit.
|
|
|
|
*
|
|
|
|
* How to use:
|
|
|
|
*
|
|
|
|
* 1. Add this to phpunit.xml:
|
|
|
|
*
|
|
|
|
* <extensions>
|
2020-04-13 07:32:19 +00:00
|
|
|
* <extension class="CodeHackerTestHook" />
|
2020-04-09 14:32:40 +00:00
|
|
|
* </extensions>
|
|
|
|
*
|
|
|
|
* 2. Add the following to the test classes:
|
|
|
|
*
|
2020-04-13 07:32:19 +00:00
|
|
|
* use Automattic\WooCommerce\Testing\CodeHacking\CodeHacker;
|
|
|
|
* use Automattic\WooCommerce\Testing\CodeHacking\Hacks\...
|
|
|
|
*
|
2020-04-09 14:32:40 +00:00
|
|
|
* public static function before_all($method_name) {
|
|
|
|
* CodeHacker::add_hack(...);
|
|
|
|
* //Register as many hacks as needed
|
|
|
|
* CodeHacker::enable();
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* $method_name is optional, 'before_all()' is also a valid method signature.
|
|
|
|
*
|
|
|
|
* You can also define a test-specific 'before_{$test_method_name}' hook.
|
|
|
|
* If both exist, first 'before_all' will be executed, then the test-specific one.
|
2020-04-10 13:36:54 +00:00
|
|
|
*
|
2020-04-13 07:32:19 +00:00
|
|
|
* 3. Additionally, you can register hacks via class/method annotations
|
|
|
|
* (note that then you don't need the `use`s anymore):
|
2020-04-10 13:36:54 +00:00
|
|
|
*
|
|
|
|
* /**
|
|
|
|
* * @hack HackClassName param1 param2
|
|
|
|
* * /
|
|
|
|
* class Some_Test
|
|
|
|
* {
|
|
|
|
* /**
|
|
|
|
* * @hack HackClassName param1 param2
|
|
|
|
* * /
|
|
|
|
* public function test_something() {
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* If the class name ends with 'Hack' you can omit that suffix in the annotation (e.g. 'Foo' instead of 'FooHack').
|
|
|
|
* Parameters specified after the class name will be passed to the class constructor.
|
|
|
|
* Hacks defined as class annotations will be applied to all tests.
|
2020-04-09 14:32:40 +00:00
|
|
|
*/
|
2020-04-12 11:09:57 +00:00
|
|
|
final class CodeHackerTestHook implements BeforeTestHook, AfterTestHook {
|
|
|
|
|
|
|
|
public function executeAfterTest( string $test, float $time ): void {
|
2020-05-06 12:40:17 +00:00
|
|
|
if ( ! CodeHacker::has_persistent_hacks() ) {
|
|
|
|
CodeHacker::disable();
|
|
|
|
}
|
2020-04-12 11:09:57 +00:00
|
|
|
}
|
2020-04-09 14:32:40 +00:00
|
|
|
|
|
|
|
public function executeBeforeTest( string $test ): void {
|
2020-04-12 11:09:57 +00:00
|
|
|
/**
|
|
|
|
* Possible formats of $test:
|
|
|
|
* TestClass::TestMethod
|
|
|
|
* TestClass::TestMethod with data set #...
|
|
|
|
* Warning
|
|
|
|
*/
|
|
|
|
$parts = explode( '::', $test );
|
|
|
|
if ( count( $parts ) < 2 ) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-09 14:32:40 +00:00
|
|
|
$class_name = $parts[0];
|
2020-04-12 11:09:57 +00:00
|
|
|
$method_name = explode( ' ', $parts[1] )[0];
|
2020-04-09 14:32:40 +00:00
|
|
|
|
2020-04-12 11:09:57 +00:00
|
|
|
CodeHacker::clear_hacks();
|
|
|
|
|
2020-04-10 13:36:54 +00:00
|
|
|
$this->execute_before_methods( $class_name, $method_name );
|
|
|
|
|
|
|
|
$has_class_annotation_hacks = $this->add_hacks_from_annotations( new ReflectionClass( $class_name ) );
|
|
|
|
$has_method_annotaion_hacks = $this->add_hacks_from_annotations( new ReflectionMethod( $class_name, $method_name ) );
|
2020-05-06 12:40:17 +00:00
|
|
|
if ( $has_class_annotation_hacks || $has_method_annotaion_hacks || CodeHacker::has_persistent_hacks() ) {
|
2020-04-10 13:36:54 +00:00
|
|
|
CodeHacker::enable();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply hacks defined in @hack annotations.
|
|
|
|
*
|
|
|
|
* @param object $reflection_object The class or method reflection object whose doc comment will be parsed.
|
|
|
|
* @return bool True if at least one valid @hack annotation was found.
|
2020-04-16 13:03:15 +00:00
|
|
|
* @throws Exception Class specified in @hack directive doesn't exist.
|
2020-04-10 13:36:54 +00:00
|
|
|
*/
|
|
|
|
private function add_hacks_from_annotations( $reflection_object ) {
|
2020-04-13 07:32:19 +00:00
|
|
|
$annotations = Test::parseAnnotations( $reflection_object->getDocComment() );
|
2020-04-10 13:36:54 +00:00
|
|
|
$hacks_added = false;
|
|
|
|
|
|
|
|
foreach ( $annotations as $id => $annotation_instances ) {
|
2020-04-16 13:03:15 +00:00
|
|
|
if ( 'hack' !== $id ) {
|
2020-04-10 13:36:54 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ( $annotation_instances as $annotation ) {
|
|
|
|
preg_match_all( '/"(?:\\\\.|[^\\\\"])*"|\S+/', $annotation, $matches );
|
|
|
|
$params = $matches[0];
|
|
|
|
|
|
|
|
$hack_class = array_shift( $params );
|
2020-04-16 13:03:15 +00:00
|
|
|
if ( false === strpos( $hack_class, '\\' ) ) {
|
2020-04-13 07:32:19 +00:00
|
|
|
$hack_class = __NAMESPACE__ . '\\Hacks\\' . $hack_class;
|
|
|
|
}
|
|
|
|
|
2020-04-10 13:36:54 +00:00
|
|
|
if ( ! class_exists( $hack_class ) ) {
|
|
|
|
$original_hack_class = $hack_class;
|
|
|
|
$hack_class .= 'Hack';
|
|
|
|
if ( ! class_exists( $hack_class ) ) {
|
|
|
|
throw new Exception( "Hack class '{$original_hack_class}' defined via annotation in {$class_name}::{$method_name} doesn't exist." );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CodeHacker::add_hack( new $hack_class( ...$params ) );
|
|
|
|
$hacks_added = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $hacks_added;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run the 'before_all' and 'before_{test_method_name}' methods in a class.
|
|
|
|
*
|
|
|
|
* @param string $class_name Test class name.
|
|
|
|
* @param string $method_name Test method name.
|
2020-04-16 13:03:15 +00:00
|
|
|
* @throws ReflectionException Error when instatiating a ReflectionClass.
|
2020-04-10 13:36:54 +00:00
|
|
|
*/
|
|
|
|
private function execute_before_methods( $class_name, $method_name ) {
|
2020-04-09 14:32:40 +00:00
|
|
|
$methods = array( 'before_all', "before_{$method_name}" );
|
|
|
|
$methods = array_filter(
|
|
|
|
$methods,
|
|
|
|
function( $item ) use ( $class_name ) {
|
|
|
|
return method_exists( $class_name, $item );
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
$rc = new ReflectionClass( $class_name );
|
|
|
|
|
|
|
|
foreach ( $methods as $method ) {
|
|
|
|
if ( 0 === $rc->getMethod( $method_name )->getNumberOfParameters() ) {
|
|
|
|
$class_name::$method();
|
|
|
|
} else {
|
|
|
|
$class_name::$method( $method_name );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-16 13:03:15 +00:00
|
|
|
|
|
|
|
// phpcs:enable Squiz.Commenting.FunctionComment.Missing, PHPCompatibility.FunctionDeclarations
|