From b38ecb37fbced3b9928699394afa9eb1dce92933 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Sat, 31 Oct 2020 14:34:27 -0400 Subject: [PATCH 1/3] Define the Functions trait The `AssertWell\PHPUnitGlobalState\Functions` trait exposes three methods for dealing with functions: 1. `defineFunction(string $name, \Closure $func): self` 2. `redefineFunction(string $name, \Closure $func): self` 3. `deleteFunction(string $name): self` This commit also adds additional documentation around runkit(7), as it's now used by both the `Constants` trait and `Functions`. Fixes #11. --- README.md | 9 +- composer.json | 5 +- composer.lock | 39 +++- docs/Constants.md | 9 +- docs/Functions.md | 158 ++++++++++++++++ docs/Runkit.md | 60 ++++++ phpstan.neon.dist | 7 + src/Concerns/Runkit.php | 41 ++++ src/Constants.php | 6 +- src/Exceptions/FunctionExistsException.php | 8 + src/Exceptions/RunkitException.php | 8 + src/Functions.php | 139 ++++++++++++++ src/Support/Runkit.php | 4 +- tests/Concerns/RunkitTest.php | 29 +++ tests/FixtureTest.php | 42 ++++- tests/FunctionsTest.php | 210 +++++++++++++++++++++ tests/Support/functions.php | 21 +++ tests/TestCase.php | 2 + 18 files changed, 771 insertions(+), 26 deletions(-) create mode 100644 docs/Functions.md create mode 100644 docs/Runkit.md create mode 100644 src/Exceptions/FunctionExistsException.php create mode 100644 src/Exceptions/RunkitException.php create mode 100644 src/Functions.php create mode 100644 tests/FunctionsTest.php create mode 100644 tests/Support/functions.php diff --git a/README.md b/README.md index 2a57891..31f70c7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ class MyTestClass extends TestCase ### Introduction to Runkit -Some of the traits will rely on [Runkit7](https://www.php.net/runkit7), a port of PHP's runkit designed to work in PHP 7.x, to rewrite code at runtime (a.k.a. "monkey-patching"). +Some of the traits will rely on [Runkit7], a port of PHP's runkit designed to work in PHP 7.x, to rewrite code at runtime (a.k.a. "monkey-patching"). For example, once a PHP constant is defined, it will normally have that value until the PHP process ends. Under normal circumstances, that's great: it prevents the value from being accidentally overwritten and/or tampered with. @@ -46,7 +46,7 @@ var_dump(SOME_CONSTANT) #=> string(10) "some value" // Now, re-define the constant. -runkit_constant_redefine('SOME_CONSTANT', 'some other value'); +runkit7_constant_redefine('SOME_CONSTANT', 'some other value'); var_dump(SOME_CONSTANT) #=> string(16) "some other value" ``` @@ -57,11 +57,14 @@ Of course, we might want a constant's original value to be restored after our te The library offers a number of traits, based on the type of global state that might need to be manipulated. -* [Constants](docs/Constants.md) (requires Runkit7) +* [Constants](docs/Constants.md) (requires [Runkit7]) * [Environment Variables](docs/EnvironmentVariables.md) +* [Functions](docs/Functions.md) (requires [Runkit7]) * [Global Variables](docs/GlobalVariables.md) ## Contributing If you're interested in contributing to the library, [please review our contributing guidelines](.github/CONTRIBUTING.md). + +[Runkit7]: docs/Runkit.md diff --git a/composer.json b/composer.json index bff8337..de4e063 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,10 @@ "autoload-dev": { "psr-4": { "Tests\\": "tests/" - } + }, + "files": [ + "tests/Support/functions.php" + ] }, "config": { "preferred-install": "dist", diff --git a/composer.lock b/composer.lock index d306ee0..289b97e 100644 --- a/composer.lock +++ b/composer.lock @@ -71,6 +71,10 @@ "stylecheck", "tests" ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, "time": "2020-06-25T14:57:39+00:00" }, { @@ -129,6 +133,10 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, "time": "2019-12-27T09:44:58+00:00" }, { @@ -171,6 +179,10 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/0.12.52" + }, "funding": [ { "url": "https://github.com/ondrejmirtes", @@ -236,6 +248,11 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, "time": "2020-10-23T02:01:07+00:00" }, { @@ -276,20 +293,24 @@ "runkit", "testing" ], + "support": { + "issues": "https://github.com/stevegrunwell/runkit7-installer/issues", + "source": "https://github.com/stevegrunwell/runkit7-installer" + }, "time": "2018-12-05T19:16:14+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "150aeb91dd9dafe13ec8416abd62e435330ca12d" + "reference": "61744927348cd391ac12f7c6b70544991275845c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/150aeb91dd9dafe13ec8416abd62e435330ca12d", - "reference": "150aeb91dd9dafe13ec8416abd62e435330ca12d", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/61744927348cd391ac12f7c6b70544991275845c", + "reference": "61744927348cd391ac12f7c6b70544991275845c", "shasum": "" }, "require": { @@ -309,9 +330,6 @@ ], "type": "symfony-bridge", "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - }, "thanks": { "name": "phpunit/phpunit", "url": "https://github.com/sebastianbergmann/phpunit" @@ -344,6 +362,9 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -358,7 +379,7 @@ "type": "tidelift" } ], - "time": "2020-10-02T12:57:56+00:00" + "time": "2020-10-24T15:53:55+00:00" } ], "aliases": [], @@ -370,5 +391,5 @@ "php": ">=5.6" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/docs/Constants.md b/docs/Constants.md index 0714092..bc3ebd3 100644 --- a/docs/Constants.md +++ b/docs/Constants.md @@ -2,15 +2,8 @@ Some applications — especially WordPress — will use [PHP constants](https://www.php.net/manual/en/language.constants.php) for configuration that should not be edited directly through the UI. -Normally, a constant cannot be redefined or removed once defined; however, [the runkit7 extension](https://www.php.net/manual/en/book.runkit7) exposes functions to modify normally immutable constructs. +Normally, a constant cannot be redefined or removed once defined; however, [the runkit7 extension](Runkit.md) exposes functions to modify normally immutable constructs. -If runkit functions are unavailable, the `Constants` trait will automatically skip tests that rely on this functionality. - -In order to install runkit7 in your development and CI environments, you may use [the installer bundled with this repo](https://github.com/stevegrunwell/runkit7-installer): - -```sh -$ sudo ./vendor/bin/install-runkit.sh -``` ## Methods diff --git a/docs/Functions.md b/docs/Functions.md new file mode 100644 index 0000000..a3eda3b --- /dev/null +++ b/docs/Functions.md @@ -0,0 +1,158 @@ +# Managing Functions + +When testing software, we often find ourselves making use of "stubs", which are objects that will return known values for given methods. + +For example, assume we're writing an integration test around how a feature behaves when an external API is unavailable, it's certainly easier to replace the HTTP response than to actually take down the API every time the test is run. + +Unfortunately, [PHPUnit's test double tools](https://phpunit.readthedocs.io/en/9.3/test-doubles.html) don't extend to functions, so we have to get creative. Fortunately, [PHP's runkit7 extension](Runkit.md), allows us to dynamically redefine functions at runtime. + + +## Methods + +As all of these methods require [runkit7](Runkit.md), tests that use these methods will automatically be marked as skipped if the extension is unavailable. + +--- + +### defineFunction() + +Define a new function for the duration of the test. + +`defineFunction(string $name, \Closure $closure): self` + +This is a wrapper around [PHP's `runkit_function_define()` function](https://www.php.net/manual/en/function.runkit-function-define.php). + +#### Parameters + +
+
$name
+
The function name.
+
$closure
+
The code for the function.
+
+ +#### Return values + +This method will return the calling class, enabling multiple methods to be chained. + +An `AssertWell\PHPUnitGlobalState\Exceptions\RunkitException` will be thrown if the given function cannot be defined. + +--- + +### redefineFunction() + +Redefine an existing function for the duration of the test. If `$name` does not exist, it will be defined. + +`redefineFunction(string $name, \Closure $closure): self` + +This is a wrapper around [PHP's `runkit_function_redefine()` function](https://www.php.net/manual/en/function.runkit-function-redefine.php). + +#### Parameters + +
+
$name
+
The function name.
+
$closure
+
The new code for the function.
+
+ +#### Return values + +This method will return the calling class, enabling multiple methods to be chained. + +An `AssertWell\PHPUnitGlobalState\Exceptions\RunkitException` will be thrown if the given function cannot be defined. + +--- + +### deleteFunction() + +Delete/undefine a function for the duration of the single test. + +`deleteFunction(string $name): self` + +#### Parameters + +
+
$name
+
The function name.
+
+ +#### Return values + +This method will return the calling class, enabling multiple methods to be chained. + + +## Examples + +### Replacing a function for a single test + +Imagine that we have two functions: `get_posts()` and `make_api_request()`, which look something like this: + +```php +/** + * Retrieve posts from the API and prepare it for templates. + * + * @return Post[] An array of Post objects. + */ +function get_posts() +{ + try { + $posts = make_api_request('/posts'); + } catch (ApiUnavailableException $e) { + error_log($e->getMessage(), E_USER_WARNING); + return []; + } + + return array_map([Post::class, 'factory'], $posts); +} + +/** + * Send a request to the API. + * + * @param string $path The API path. + * @param mixed[] $args Arguments to pass with the request. + * + * @return array[] + */ +function make_api_request($path, $args = []) +{ + /* + * A bunch of pre-check conditions, sanitization, merging with default + * values, etc. + * + * Then we'll make the actual request, and finally check the results. + */ + if ($response_code >= 500) { + throw new ApiUnavailableException('Received a 5xx error from the API.'); + } + + // More logic before finally returning the response. +} +``` + +We're trying to write unit tests for `get_posts()`, but the path we want to test is what happens when `make_api_request()` returns throws an `ApiUnavailableException`. + +Now, assume that we don't have an easy way to emulate a 5xx status code from the API to cause `make_api_request()` to throw an `ApiUnavailableException`. Furthermore, we don't actually _want_ our tests making external requests, as that would add latency, external dependencies, and potentially cost money if it's a pay-per-usage service. + +Instead of weighing down our tests with a ton of code to make `make_api_request()` throw the desired exception, we can simply replace the function: + +```php +use AssertWell\PHPUnitGlobalState\Functions; +use PHPUnit\Framework\TestCase; + +class MyTestClass extends TestCase +{ + use Functions; + + /** + * @test + */ + public function get_posts_should_return_an_empty_array_if_the_API_request_fails() + { + $this->redefineFunction('make_api_request', function () { + throw new ApiUnavailableException('API is unavailable.'); + }); + + $this->assertEmpty(get_posts()); + } +} +``` diff --git a/docs/Runkit.md b/docs/Runkit.md new file mode 100644 index 0000000..0fad499 --- /dev/null +++ b/docs/Runkit.md @@ -0,0 +1,60 @@ +# PHP's runkit and runkit7 extensions + +> For all those things you… probably shouldn't have been doing anyway… but surely do! + +In the PHP 5.x days, we had [the runkit extension](http://pecl.php.net/package/runkit) for dynamically redefining things that _shouldn't_ normally be redefined within the PHP runtime. + +For example, if you needed to change the value of a constant, your options were slim-to-nil before runkit came along. With the extension installed, however, you could now redefine that which was never meant to be redefined. + +With the release of PHP 7.x, [Tyson Andre](https://github.com/TysonAndre) forked runkit to create [runkit7](https://github.com/runkit7/runkit7), a PHP 7.x-compatible version of the extension. + + +## You really shouldn't be using runkit… + +The runkit(7) extension is an immensely-powerful tool, but if you're not careful it can be the source of a lot of pain within your codebase. + +Generally speaking, **if you're using runkit in production code, you're probably approaching the problem in the wrong way.** + +That being said, runkit can be _amazing_ for automated tests, as we can dynamically change configurations and behaviors to emulate certain situations. If you're testing older or poorly-architected applications, runkit can mean the difference between a comprehensive test suite and one that leaves a lot of paths uncovered. + +Using runkit should probably never be your first approach, but in certain situations it's by-far the cleanest. + +Remember: **with great power comes great responsibility!!** + + +## Installation + +Both runkit and runkit7 can be installed in your environment via [PECL](https://pecl.php.net/): + +```sh +# For PHP 5.x +pecl install runkit + +# For PHP 7.x +pecl install runkit7 +``` + +Depending on your environment, you may also need to take additional steps to load the extension after installation, which will be detailed in the shell output from `pecl install`. + +If you'd like to automate this process further, you may also consider [installing stevegrunwell/runkit7-installer](https://github.com/stevegrunwell/runkit7-installer#installation) as a Composer dependency in your project. + + +## Using runkit and runkit7 in the same test suite + +More recent versions of runkit7 have introduced `runkit7_`-prefixed functions, and their `runkit_` counterparts are aliased to the newer versions. + +For example, `runkit_function_redefine()` is an alias for `runkit7_function_redefine()`. + +However, static code analysis tools like [PHPStan](https://phpstan.org/) will often throw warnings about the `runkit_` versions of the functions being undefined, and the corresponding pages are being removed from [php.net](https://php.net). + +To get around these issues, this library includes the `AssertWell\PHPUnitGlobalState\Support\Runkit` class, which proxies static method calls to runkit based what's available: + +```php +use AssertWell\PHPUnitGlobalState\Support\Runkit; + +/* + * Use runkit7_constant_redefine() on PHP 7.x, + * runkit_constant_redefine() for PHP 5.x. + */ +Runkit::constant_redefine('SOME_CONSTANT', 'some value'); +``` diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9a51eff..a4cb97c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -17,3 +17,10 @@ parameters: - message: '#Parameter \#1 \$function of function call_user_func_array expects callable\(\): mixed, string given\.#' path: src/Support/Runkit.php + + # Dynamically-defined functions. + - + message: '#Function \S+ not found\.$#' + paths: + - tests/FixtureTest.php + - tests/FunctionsTest.php diff --git a/src/Concerns/Runkit.php b/src/Concerns/Runkit.php index 126e0e2..3c89908 100644 --- a/src/Concerns/Runkit.php +++ b/src/Concerns/Runkit.php @@ -6,6 +6,13 @@ trait Runkit { + /** + * A namespace used to move things out of the way for the duration of a test. + * + * @var string + */ + private $runkitNamespace; + /** * Mark a test as skipped if Runkit is not available. * @@ -35,4 +42,38 @@ protected function isRunkitAvailable() return function_exists('runkit7_constant_redefine') || function_exists('runkit_constant_redefine'); } + + /** + * Get the current runkit namespace. + * + * If the property is currently empty, one will be created. + * + * @return string The namespace (with trailing backslash) where we're moving functions, + * constants, etc. during tests. + */ + protected function getRunkitNamespace() + { + if (empty($this->runkitNamespace)) { + $this->runkitNamespace = uniqid(__NAMESPACE__ . '\\runkit_') . '\\'; + } + + return $this->runkitNamespace; + } + + /** + * Namespace the given reference. + * + * @param string $var The item to be moved into the temporary test namespace. + * + * @return string The newly-namespaced item. + */ + protected function runkitNamespace($var) + { + // Strip leading backslashes. + if (0 === mb_strpos($var, '\\')) { + $var = mb_substr($var, 1); + } + + return $this->getRunkitNamespace() . $var; + } } diff --git a/src/Constants.php b/src/Constants.php index 2e697f3..00ecab3 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -32,12 +32,16 @@ protected function restoreConstants() } else { define($name, $value); } + + unset($this->constants['updated'][$name]); } - foreach ($this->constants['created'] as $name) { + foreach ($this->constants['created'] as $key => $name) { if (defined($name)) { Runkit::constant_remove($name); } + + unset($this->constants['created'][$key]); } } diff --git a/src/Exceptions/FunctionExistsException.php b/src/Exceptions/FunctionExistsException.php new file mode 100644 index 0000000..6d000a5 --- /dev/null +++ b/src/Exceptions/FunctionExistsException.php @@ -0,0 +1,8 @@ + [], + 'redefined' => [], + ]; + + /** + * @after + * + * @return void + */ + protected function restoreFunctions() + { + // Reset anything that was modified. + array_walk($this->functions['redefined'], function ($original, $name) { + if (function_exists($name)) { + Runkit::function_remove($name); + } + + // Put the original back into place. + Runkit::function_rename($original, $name); + + unset($this->functions['redefined'][$name]); + }); + + array_map([Runkit::class, 'function_remove'], $this->functions['defined']); + $this->functions['defined'] = []; + } + + /** + * Define a new function. + * + * @throws \AssertWell\PHPUnitGlobalState\Exceptions\FunctionExistsException + * @throws \AssertWell\PHPUnitGlobalState\Exceptions\RunkitException + * + * @param string $name The function name. + * @param \Closure $closure The function body. + * + * @return self + */ + protected function defineFunction($name, \Closure $closure) + { + if (function_exists($name)) { + throw new FunctionExistsException(sprintf( + 'Function %1$s() already exists. You may redefine it using %2$s::redefineFunction() instead.', + $name, + get_class($this) + )); + } + + $this->requiresRunkit('defineFunction() requires Runkit be available, skipping.'); + + if (! Runkit::function_add($name, $closure)) { + throw new RunkitException(sprintf('Unable to define function %1$s().', $name)); + } + + $this->functions['defined'][] = $name; + + return $this; + } + + /** + * Redefine an existing function. + * + * If the function doesn't yet exist, it will be defined. + * + * @param string $name The function name to be redefined. + * @param \Closure $closure The new function body. + * + * @return self + */ + protected function redefineFunction($name, \Closure $closure) + { + if (! function_exists($name)) { + return $this->defineFunction($name, $closure); + } + + $this->requiresRunkit('redefineFunction() requires Runkit be available, skipping.'); + + // Back up the original version of the function. + if (! isset($this->functions['redefined'][$name])) { + $namespaced = $this->runkitNamespace($name); + + if (! Runkit::function_rename($name, $namespaced)) { + throw new RunkitException(sprintf('Unable to back up %1$s(), aborting.', $name)); + } + + $this->functions['redefined'][$name] = $namespaced; + + if (! Runkit::function_add($name, $closure)) { + throw new RunkitException(sprintf('Unable to redefine function %1$s().', $name)); + } + } else { + Runkit::function_redefine($name, $closure); + } + + return $this; + } + + /** + * Delete an existing function. + * + * @param string $name The function to be deleted. + * + * @return self + */ + protected function deleteFunction($name) + { + if (! function_exists($name)) { + return $this; + } + + $namespaced = $this->runkitNamespace($name); + + if (! Runkit::function_rename($name, $namespaced)) { + throw new RunkitException(sprintf('Unable to back up %1$s(), aborting.', $name)); + } + + $this->functions['redefined'][$name] = $namespaced; + + return $this; + } +} diff --git a/src/Support/Runkit.php b/src/Support/Runkit.php index 5fbf103..48a7146 100644 --- a/src/Support/Runkit.php +++ b/src/Support/Runkit.php @@ -13,8 +13,10 @@ * @method static bool constant_redefine(string $constname, mixed $value, int $newVisibility = NULL) * @method static bool constant_remove(string $constname) * @method static bool function_add(string $funcname, string $arglist, string $code, bool $return_by_reference = NULL, string $doc_comment = NULL, string $return_type, bool $is_strict = NULL) + * @method static bool function_add(string $funcname, \Closure $closure, string $doc_comment = NULL, string $return_type = NULL, bool $is_strict = NULL) * @method static bool function_copy(string $funcname, string $targetname) * @method static bool function_redefine(string $funcname, string $arglist, string $code, bool $return_by_reference = NULL, string $doc_comment = NULL, string $return_type = NULL, bool $is_strict) + * @method static bool function_redefine(string $funcname, \Closure $closure, string $doc_comment = NULL, string $return_type = NULL, string $is_strict = NULL) * @method static bool function_remove(string $funcname) * @method static bool function_rename(string $funcname, string $newname) * @method static bool import(string $filename, int $flags = NULL) @@ -51,7 +53,7 @@ public static function __callStatic($name, array $args = []) } throw new \BadFunctionCallException(sprintf( - 'Runkit7 does not include a runkit7_%1$s() function.', + 'Neither runkit7_%1$s() nor runkit_%1$s() are defined.', $name )); } diff --git a/tests/Concerns/RunkitTest.php b/tests/Concerns/RunkitTest.php index 1620e58..120f449 100644 --- a/tests/Concerns/RunkitTest.php +++ b/tests/Concerns/RunkitTest.php @@ -66,4 +66,33 @@ public function it_should_skip_tests_that_require_runkit_if_it_is_unavailable() $this->fail('Did not catch the expected SkippedTestError.'); } + + /** + * @test + */ + public function it_should_be_able_to_namespace_values() + { + $namespace = new \ReflectionMethod($this->instance, 'getRunkitNamespace'); + $namespace->setAccessible(true); + $namespace = $namespace->invoke($this->instance); + + $method = new \ReflectionMethod($this->instance, 'runkitNamespace'); + $method->setAccessible(true); + + $this->assertSame( + $namespace . 'some_global_function', + $method->invoke($this->instance, 'some_global_function'), + 'The global namespace should be eligible.' + ); + $this->assertSame( + $namespace . 'Some\\Namespaced\\function_to_move', + $method->invoke($this->instance, 'Some\\Namespaced\\function_to_move'), + 'Namespaces should be preserved.' + ); + $this->assertSame( + $namespace . 'Some\\Namespaced\\function_with_leading_slashes', + $method->invoke($this->instance, '\\Some\\Namespaced\\function_with_leading_slashes'), + 'Leading slashes should be stripped.' + ); + } } diff --git a/tests/FixtureTest.php b/tests/FixtureTest.php index 5e153b2..534c121 100644 --- a/tests/FixtureTest.php +++ b/tests/FixtureTest.php @@ -27,6 +27,10 @@ public function doSetUp() $this->setConstant('FIXTURE_SETUP_CONSTANT', true); $this->setEnvironmentVariable('FIXTURE_SETUP_ENV', 'abc'); $this->setGlobalVariable('FIXTURE_SETUP_GLOBAL', true); + + $this->defineFunction('fixture_setup_function', function () { + return 'abc'; + }); } /** @@ -37,13 +41,17 @@ protected function defineInitialValues() $this->setConstant('FIXTURE_BEFORE_CONSTANT', true); $this->setEnvironmentVariable('FIXTURE_BEFORE_ENV', 'xyz'); $this->setGlobalVariable('FIXTURE_BEFORE_GLOBAL', true); + + $this->defineFunction('fixture_before_function', function () { + return 'xyz'; + }); } /** * @test * @group Constants */ - public function it_should_permit_constants_to_be_set_in_fixtures_method() + public function it_should_permit_constants_to_be_set_in_fixtures() { $this->assertTrue( defined('FIXTURE_SETUP_CONSTANT'), @@ -69,7 +77,7 @@ public function it_should_permit_constants_to_be_set_in_fixtures_method() * @test * @group EnvironmentVariables */ - public function it_should_permit_environment_variables_to_be_set_in_fixtures_method() + public function it_should_permit_environment_variables_to_be_set_in_fixtures() { $this->assertSame( 'abc', @@ -93,11 +101,39 @@ public function it_should_permit_environment_variables_to_be_set_in_fixtures_met ); } + /** + * @test + * @group Functions + */ + public function it_should_permit_functions_to_be_set_in_fixtures() + { + $this->assertSame( + 'abc', + fixture_setup_function(), + 'The function should have been defined in the setUp() method.' + ); + $this->assertSame( + 'xyz', + fixture_before_function(), + 'The function should have been defined in the @before method.' + ); + + $this->restoreFunctions(); + $this->assertFalse( + function_exists('fixture_setup_function'), + 'The function should have been undefined by restoreFunctions().' + ); + $this->assertFalse( + function_exists('fixture_before_function'), + 'The function should have been undefined by restoreFunctions().' + ); + } + /** * @test * @group GlobalVariables */ - public function it_should_permit_global_variables_to_be_set_in_fixtures_method() + public function it_should_permit_global_variables_to_be_set_in_fixtures() { $this->assertTrue( isset($GLOBALS['FIXTURE_SETUP_GLOBAL']), diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php new file mode 100644 index 0000000..6cb34cb --- /dev/null +++ b/tests/FunctionsTest.php @@ -0,0 +1,210 @@ +requiresRunkit('This test depends on runkit being available.'); + } + + /** + * @test + * @testdox defineFunction() should be able to define a new function + */ + public function defineFunction_should_be_able_to_define_a_new_function() + { + $this->assertFalse(function_exists('my_custom_function')); + + $this->defineFunction('my_custom_function', function ($return) { + return $return; + }); + + $this->assertSame(123, my_custom_function(123)); + + $this->restoreFunctions(); + $this->assertFalse(function_exists('my_custom_function'), 'The new function should have been undefined.'); + } + + /** + * @test + * @testdox defineFunction() should throw a warning if the function already exists + */ + public function defineFunction_should_throw_a_warning_if_the_function_already_exists() + { + $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); + $signature = (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')); + + $this->expectException(FunctionExistsException::class); + $this->defineFunction('Tests\Support\sum_three_numbers', function ($return) { + return $return; + }); + + $this->assertSame( + $signature, + (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')), + 'The original function should have been left untouched.' + ); + } + + /** + * @test + * @testdox redefineFunction() should be able to redefine an existing function + */ + public function redefineFunction_should_be_able_to_redefine_existing_functions() + { + $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); + $signature = (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')); + + $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + return 123; + }); + + $this->assertSame(123, sum_three_numbers(1, 2, 3)); + + $this->restoreFunctions(); + $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); + $this->assertSame( + $signature, + (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')), + 'The original function definition should have been restored.' + ); + } + + /** + * @test + * @testdox redefineFunction() should be able to redefine an existing function + */ + public function redefineFunction_should_be_able_to_redefine_newly_defined_functions() + { + $this->defineFunction('my_test_function', function () { + return 'abc'; + }); + $this->redefineFunction('my_test_function', function () { + return 'xyz'; + }); + + $this->assertSame('xyz', my_test_function()); + + $this->restoreFunctions(); + $this->assertFalse( + function_exists('my_test_function'), + 'The newly-created function should still be removed.' + ); + } + + /** + * @test + * @testdox redefineFunction() should be able to redefine an existing function multiple times + * @depends redefineFunction_should_be_able_to_redefine_existing_functions + */ + public function redefineFunction_should_be_able_to_redefine_existing_functions_multiple_times() + { + $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); + $signature = (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')); + + $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + return 'first'; + }); + $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + return 'second'; + }); + $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + return 'third'; + }); + + $this->assertSame( + 'third', + sum_three_numbers(1, 2, 3), + 'Expected the latest re-definition to be used.' + ); + + $this->restoreFunctions(); + $this->assertSame( + $signature, + (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')), + 'The original function definition should have been restored.' + ); + } + + /** + * @test + * @testdox redefineFunction() should define functions if they do not exist + * @depends defineFunction_should_be_able_to_define_a_new_function + */ + public function redefineFunction_should_define_functions_if_they_do_not_exist() + { + $this->assertFalse(function_exists('my_custom_function')); + + $this->redefineFunction('my_custom_function', function ($return) { + return $return; + }); + + $this->assertSame(123, my_custom_function(123)); + + $this->restoreFunctions(); + $this->assertFalse(function_exists('my_custom_function'), 'The new function should have been undefined.'); + } + + /** + * @test + * @testdox deleteFunction() should be able to delete functions + */ + public function deleteFunction_should_be_able_to_delete_functions() + { + $this->assertTrue( + function_exists('Tests\Support\sum_three_numbers'), + 'Test is predicated on this function existing.' + ); + + $this->deleteFunction('Tests\Support\sum_three_numbers'); + $this->assertFalse( + function_exists('Tests\Support\sum_three_numbers'), + 'The function should have been deleted.' + ); + + $this->restoreFunctions(); + $this->assertTrue( + function_exists('Tests\Support\sum_three_numbers'), + 'The function should have been restored.' + ); + } + + /** + * @test + * @testdox deleteFunction() should do nothing if the function does not exist + */ + public function deleteFunction_should_do_nothing_if_the_function_does_not_exist() + { + $this->assertFalse( + function_exists('Tests\Support\sum_three_numbers_again'), + 'Test is predicated on this function NOT existing.' + ); + + $this->deleteFunction('Tests\Support\sum_three_numbers_again'); + $this->assertFalse( + function_exists('Tests\Support\sum_three_numbers_again'), + 'Deleting a non-existent function should not do anything.' + ); + + $this->restoreFunctions(); + $this->assertFalse( + function_exists('Tests\Support\sum_three_numbers_again'), + 'Nothing should be restored as there was nothing to begin with.' + ); + } +} diff --git a/tests/Support/functions.php b/tests/Support/functions.php new file mode 100644 index 0000000..201485a --- /dev/null +++ b/tests/Support/functions.php @@ -0,0 +1,21 @@ + Date: Sat, 31 Oct 2020 16:19:00 -0400 Subject: [PATCH 2/3] To avoid users from having to deal with conflict resolution, do away with the Runkit trait in favor of more static methods on the Runkit support class --- composer.json | 2 +- phpstan.neon.dist | 4 ++ src/Concerns/Runkit.php | 79 --------------------- src/Constants.php | 12 ++-- src/Functions.php | 16 +++-- src/Support/Runkit.php | 64 +++++++++++++++++ tests/Concerns/RunkitTest.php | 98 -------------------------- tests/ConstantsTest.php | 18 +++-- tests/FunctionsTest.php | 54 +++++++------- tests/Support/RunkitTest.php | 59 ++++++++++++++++ tests/{Support => stubs}/functions.php | 2 +- 11 files changed, 188 insertions(+), 220 deletions(-) delete mode 100644 src/Concerns/Runkit.php delete mode 100644 tests/Concerns/RunkitTest.php create mode 100644 tests/Support/RunkitTest.php rename tests/{Support => stubs}/functions.php (90%) diff --git a/composer.json b/composer.json index de4e063..c0f3d02 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "Tests\\": "tests/" }, "files": [ - "tests/Support/functions.php" + "tests/stubs/functions.php" ] }, "config": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a4cb97c..670eeba 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -24,3 +24,7 @@ parameters: paths: - tests/FixtureTest.php - tests/FunctionsTest.php + + - + message: '#Call to an undefined static method \S+#' + path: tests/Support/RunkitTest.php diff --git a/src/Concerns/Runkit.php b/src/Concerns/Runkit.php deleted file mode 100644 index 3c89908..0000000 --- a/src/Concerns/Runkit.php +++ /dev/null @@ -1,79 +0,0 @@ -isRunkitAvailable()) { - return; - } - - throw new SkippedTestError($message ?: 'This test requires Runkit, skipping.'); - } - - /** - * Determine whether or not Runkit is available in the current environment. - * - * @return bool - */ - protected function isRunkitAvailable() - { - return function_exists('runkit7_constant_redefine') - || function_exists('runkit_constant_redefine'); - } - - /** - * Get the current runkit namespace. - * - * If the property is currently empty, one will be created. - * - * @return string The namespace (with trailing backslash) where we're moving functions, - * constants, etc. during tests. - */ - protected function getRunkitNamespace() - { - if (empty($this->runkitNamespace)) { - $this->runkitNamespace = uniqid(__NAMESPACE__ . '\\runkit_') . '\\'; - } - - return $this->runkitNamespace; - } - - /** - * Namespace the given reference. - * - * @param string $var The item to be moved into the temporary test namespace. - * - * @return string The newly-namespaced item. - */ - protected function runkitNamespace($var) - { - // Strip leading backslashes. - if (0 === mb_strpos($var, '\\')) { - $var = mb_substr($var, 1); - } - - return $this->getRunkitNamespace() . $var; - } -} diff --git a/src/Constants.php b/src/Constants.php index 00ecab3..20a099e 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -7,8 +7,6 @@ trait Constants { - use Concerns\Runkit; - /** * All constants being handled by this trait. * @@ -43,6 +41,8 @@ protected function restoreConstants() unset($this->constants['created'][$key]); } + + Runkit::reset(); } /** @@ -59,7 +59,9 @@ protected function restoreConstants() */ protected function setConstant($name, $value = null) { - $this->requiresRunkit('setConstant() requires Runkit be available, skipping.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('setConstant() requires Runkit be available, skipping.'); + } if (defined($name)) { if (! isset($this->constants['updated'][$name])) { @@ -96,7 +98,9 @@ protected function deleteConstant($name) return $this; } - $this->requiresRunkit('deleteConstant() requires Runkit be available, skipping.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('deleteConstant() requires Runkit be available, skipping.'); + } if (! isset($this->constants[$name])) { $this->constants['updated'][$name] = constant($name); diff --git a/src/Functions.php b/src/Functions.php index 22cf953..37a053a 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -8,8 +8,6 @@ trait Functions { - use Concerns\Runkit; - /** * All functions being handled by this trait. * @@ -41,6 +39,8 @@ protected function restoreFunctions() array_map([Runkit::class, 'function_remove'], $this->functions['defined']); $this->functions['defined'] = []; + + Runkit::reset(); } /** @@ -64,7 +64,9 @@ protected function defineFunction($name, \Closure $closure) )); } - $this->requiresRunkit('defineFunction() requires Runkit be available, skipping.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('defineFunction() requires Runkit be available, skipping.'); + } if (! Runkit::function_add($name, $closure)) { throw new RunkitException(sprintf('Unable to define function %1$s().', $name)); @@ -91,11 +93,13 @@ protected function redefineFunction($name, \Closure $closure) return $this->defineFunction($name, $closure); } - $this->requiresRunkit('redefineFunction() requires Runkit be available, skipping.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('redefineFunction() requires Runkit be available, skipping.'); + } // Back up the original version of the function. if (! isset($this->functions['redefined'][$name])) { - $namespaced = $this->runkitNamespace($name); + $namespaced = Runkit::makeNamespaced($name); if (! Runkit::function_rename($name, $namespaced)) { throw new RunkitException(sprintf('Unable to back up %1$s(), aborting.', $name)); @@ -126,7 +130,7 @@ protected function deleteFunction($name) return $this; } - $namespaced = $this->runkitNamespace($name); + $namespaced = Runkit::makeNamespaced($name); if (! Runkit::function_rename($name, $namespaced)) { throw new RunkitException(sprintf('Unable to back up %1$s(), aborting.', $name)); diff --git a/src/Support/Runkit.php b/src/Support/Runkit.php index 48a7146..eb3fa69 100644 --- a/src/Support/Runkit.php +++ b/src/Support/Runkit.php @@ -32,6 +32,13 @@ */ class Runkit { + /** + * A namespace used to move things out of the way for the duration of a test. + * + * @var string + */ + private static $namespace; + /** * Dynamically alias methods to the underlying Runkit functions. * @@ -57,4 +64,61 @@ public static function __callStatic($name, array $args = []) $name )); } + + /** + * Determine whether or not Runkit is available in the current environment. + * + * @return bool + */ + public static function isAvailable() + { + return function_exists('runkit7_constant_redefine') + || function_exists('runkit_constant_redefine'); + } + + /** + * Get the current runkit namespace. + * + * If the property is currently empty, one will be created. + * + * @return string The namespace (with trailing backslash) where we're moving functions, + * constants, etc. during tests. + */ + public static function getNamespace() + { + if (empty(self::$namespace)) { + self::$namespace = uniqid(__NAMESPACE__ . '\\runkit_') . '\\'; + } + + return self::$namespace; + } + + /** + * Namespace the given reference. + * + * @param string $var The item to be moved into the temporary test namespace. + * + * @return string The newly-namespaced item. + */ + public static function makeNamespaced($var) + { + // Strip leading backslashes. + if (0 === mb_strpos($var, '\\')) { + $var = mb_substr($var, 1); + } + + return self::getNamespace() . $var; + } + + /** + * Reset static properties. + * + * This is helpful to run before tests in case self::$namespace gets polluted. + * + * @return void + */ + public static function reset() + { + self::$namespace = ''; + } } diff --git a/tests/Concerns/RunkitTest.php b/tests/Concerns/RunkitTest.php deleted file mode 100644 index 120f449..0000000 --- a/tests/Concerns/RunkitTest.php +++ /dev/null @@ -1,98 +0,0 @@ -instance = $this->getMockForTrait(Runkit::class, [], '', true, true, true, [ - 'isRunkitAvailable', - ]); - } - - /** - * @test - */ - public function it_should_permit_tests_to_run_if_runkit_is_available() - { - $this->instance->expects($this->once()) - ->method('isRunkitAvailable') - ->willReturn(true); - - $method = new \ReflectionMethod($this->instance, 'requiresRunkit'); - $method->setAccessible(true); - - $this->assertNull($method->invoke($this->instance)); - } - - /** - * @test - */ - public function it_should_skip_tests_that_require_runkit_if_it_is_unavailable() - { - $this->instance->expects($this->once()) - ->method('isRunkitAvailable') - ->willReturn(false); - - $method = new \ReflectionMethod($this->instance, 'requiresRunkit'); - $method->setAccessible(true); - - // Older versions of PHPUnit will actually try to mark this as skipped. - try { - $method->invoke($this->instance); - } catch (SkippedTestError $e) { - $this->assertInstanceOf(SkippedTestError::class, $e); - return; - } - - $this->fail('Did not catch the expected SkippedTestError.'); - } - - /** - * @test - */ - public function it_should_be_able_to_namespace_values() - { - $namespace = new \ReflectionMethod($this->instance, 'getRunkitNamespace'); - $namespace->setAccessible(true); - $namespace = $namespace->invoke($this->instance); - - $method = new \ReflectionMethod($this->instance, 'runkitNamespace'); - $method->setAccessible(true); - - $this->assertSame( - $namespace . 'some_global_function', - $method->invoke($this->instance, 'some_global_function'), - 'The global namespace should be eligible.' - ); - $this->assertSame( - $namespace . 'Some\\Namespaced\\function_to_move', - $method->invoke($this->instance, 'Some\\Namespaced\\function_to_move'), - 'Namespaces should be preserved.' - ); - $this->assertSame( - $namespace . 'Some\\Namespaced\\function_with_leading_slashes', - $method->invoke($this->instance, '\\Some\\Namespaced\\function_with_leading_slashes'), - 'Leading slashes should be stripped.' - ); - } -} diff --git a/tests/ConstantsTest.php b/tests/ConstantsTest.php index 11d02ab..e3599d2 100644 --- a/tests/ConstantsTest.php +++ b/tests/ConstantsTest.php @@ -3,10 +3,10 @@ namespace Tests; use AssertWell\PHPUnitGlobalState\Exceptions\RedefineException; +use AssertWell\PHPUnitGlobalState\Support\Runkit; use PHPUnit\Framework\SkippedTestError; /** - * @covers AssertWell\PHPUnitGlobalState\Concerns\Runkit * @covers AssertWell\PHPUnitGlobalState\Constants * * @group Constants @@ -31,7 +31,9 @@ public static function defineConstants() */ public function setConstant_should_be_able_to_handle_newly_defined_constants() { - $this->requiresRunkit('This test depends on runkit being available.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('This test depends on runkit being available.'); + } $this->assertFalse(defined('SOME_CONSTANT')); @@ -48,7 +50,9 @@ public function setConstant_should_be_able_to_handle_newly_defined_constants() */ public function setConstant_should_be_able_to_redefine_existing_constants() { - $this->requiresRunkit('This test depends on runkit being available.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('This test depends on runkit being available.'); + } $this->setConstant('EXISTING_CONSTANT', 'some other value'); $this->assertSame('some other value', constant('EXISTING_CONSTANT')); @@ -67,7 +71,9 @@ public function setConstant_should_be_able_to_redefine_existing_constants() */ public function setConstant_should_throw_an_exception_if_it_cannot_redefine_a_constant() { - $this->requiresRunkit('This test depends on runkit being available.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('This test depends on runkit being available.'); + } $this->expectException(RedefineException::class); $this->setConstant('EXISTING_CONSTANT', (object) ['some' => 'object']); @@ -85,7 +91,9 @@ public function setConstant_should_throw_an_exception_if_it_cannot_redefine_a_co */ public function deleteConstant_should_remove_an_existing_constant() { - $this->requiresRunkit('This test depends on runkit being available.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('This test depends on runkit being available.'); + } $this->deleteConstant('DELETE_THIS_CONSTANT'); $this->assertFalse(defined('DELETE_THIS_CONSTANT')); diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 6cb34cb..3aac327 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -3,11 +3,11 @@ namespace Tests; use AssertWell\PHPUnitGlobalState\Exceptions\FunctionExistsException; +use AssertWell\PHPUnitGlobalState\Support\Runkit; -use function Tests\Support\sum_three_numbers; +use function Tests\Stubs\sum_three_numbers; /** - * @covers AssertWell\PHPUnitGlobalState\Concerns\Runkit * @covers AssertWell\PHPUnitGlobalState\Functions * * @group Functions @@ -19,7 +19,9 @@ class FunctionsTest extends TestCase */ protected function verifyRunkitIsAvailable() { - $this->requiresRunkit('This test depends on runkit being available.'); + if (! Runkit::isAvailable()) { + $this->markTestSkipped('This test depends on runkit being available.'); + } } /** @@ -46,17 +48,17 @@ public function defineFunction_should_be_able_to_define_a_new_function() */ public function defineFunction_should_throw_a_warning_if_the_function_already_exists() { - $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); - $signature = (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')); + $this->assertTrue(function_exists('Tests\\Stubs\\sum_three_numbers')); + $signature = (string) (new \ReflectionFunction('Tests\\Stubs\\sum_three_numbers')); $this->expectException(FunctionExistsException::class); - $this->defineFunction('Tests\Support\sum_three_numbers', function ($return) { + $this->defineFunction('Tests\\Stubs\\sum_three_numbers', function ($return) { return $return; }); $this->assertSame( $signature, - (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')), + (string) (new \ReflectionFunction('Tests\\Stubs\\sum_three_numbers')), 'The original function should have been left untouched.' ); } @@ -67,20 +69,20 @@ public function defineFunction_should_throw_a_warning_if_the_function_already_ex */ public function redefineFunction_should_be_able_to_redefine_existing_functions() { - $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); - $signature = (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')); + $this->assertTrue(function_exists('Tests\\Stubs\\sum_three_numbers')); + $signature = (string) (new \ReflectionFunction('Tests\\Stubs\\sum_three_numbers')); - $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + $this->redefineFunction('Tests\\Stubs\\sum_three_numbers', function () { return 123; }); $this->assertSame(123, sum_three_numbers(1, 2, 3)); $this->restoreFunctions(); - $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); + $this->assertTrue(function_exists('Tests\\Stubs\\sum_three_numbers')); $this->assertSame( $signature, - (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')), + (string) (new \ReflectionFunction('Tests\\Stubs\\sum_three_numbers')), 'The original function definition should have been restored.' ); } @@ -114,16 +116,16 @@ function_exists('my_test_function'), */ public function redefineFunction_should_be_able_to_redefine_existing_functions_multiple_times() { - $this->assertTrue(function_exists('Tests\Support\sum_three_numbers')); - $signature = (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')); + $this->assertTrue(function_exists('Tests\\Stubs\\sum_three_numbers')); + $signature = (string) (new \ReflectionFunction('Tests\\Stubs\\sum_three_numbers')); - $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + $this->redefineFunction('Tests\\Stubs\\sum_three_numbers', function () { return 'first'; }); - $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + $this->redefineFunction('Tests\\Stubs\\sum_three_numbers', function () { return 'second'; }); - $this->redefineFunction('Tests\Support\sum_three_numbers', function () { + $this->redefineFunction('Tests\\Stubs\\sum_three_numbers', function () { return 'third'; }); @@ -136,7 +138,7 @@ public function redefineFunction_should_be_able_to_redefine_existing_functions_m $this->restoreFunctions(); $this->assertSame( $signature, - (string) (new \ReflectionFunction('Tests\Support\sum_three_numbers')), + (string) (new \ReflectionFunction('Tests\\Stubs\\sum_three_numbers')), 'The original function definition should have been restored.' ); } @@ -167,19 +169,19 @@ public function redefineFunction_should_define_functions_if_they_do_not_exist() public function deleteFunction_should_be_able_to_delete_functions() { $this->assertTrue( - function_exists('Tests\Support\sum_three_numbers'), + function_exists('Tests\\Stubs\\sum_three_numbers'), 'Test is predicated on this function existing.' ); - $this->deleteFunction('Tests\Support\sum_three_numbers'); + $this->deleteFunction('Tests\\Stubs\\sum_three_numbers'); $this->assertFalse( - function_exists('Tests\Support\sum_three_numbers'), + function_exists('Tests\\Stubs\\sum_three_numbers'), 'The function should have been deleted.' ); $this->restoreFunctions(); $this->assertTrue( - function_exists('Tests\Support\sum_three_numbers'), + function_exists('Tests\\Stubs\\sum_three_numbers'), 'The function should have been restored.' ); } @@ -191,19 +193,19 @@ function_exists('Tests\Support\sum_three_numbers'), public function deleteFunction_should_do_nothing_if_the_function_does_not_exist() { $this->assertFalse( - function_exists('Tests\Support\sum_three_numbers_again'), + function_exists('Tests\\Stubs\\sum_three_numbers_again'), 'Test is predicated on this function NOT existing.' ); - $this->deleteFunction('Tests\Support\sum_three_numbers_again'); + $this->deleteFunction('Tests\\Stubs\\sum_three_numbers_again'); $this->assertFalse( - function_exists('Tests\Support\sum_three_numbers_again'), + function_exists('Tests\\Stubs\\sum_three_numbers_again'), 'Deleting a non-existent function should not do anything.' ); $this->restoreFunctions(); $this->assertFalse( - function_exists('Tests\Support\sum_three_numbers_again'), + function_exists('Tests\\Stubs\\sum_three_numbers_again'), 'Nothing should be restored as there was nothing to begin with.' ); } diff --git a/tests/Support/RunkitTest.php b/tests/Support/RunkitTest.php new file mode 100644 index 0000000..b9474cf --- /dev/null +++ b/tests/Support/RunkitTest.php @@ -0,0 +1,59 @@ +expectException(\BadFunctionCallException::class); + + Runkit::a_function_that_does_not_exist(); + } + + /** + * @test + */ + public function getNamespace_should_return_the_same_value_on_subsequent_calls() + { + $namespace = Runkit::getNamespace(); + + $this->assertSame(Runkit::getNamespace(), $namespace); + } + + /** + * @test + */ + public function makeNamespaced_should_return_the_given_reference_with_a_prefixed_namespace() + { + $namespace = Runkit::getNamespace(); + + $this->assertSame( + $namespace . 'some_global_function', + Runkit::makeNamespaced('some_global_function'), + 'The global namespace should be eligible.' + ); + $this->assertSame( + $namespace . 'Some\\Namespaced\\function_to_move', + Runkit::makeNamespaced('Some\\Namespaced\\function_to_move'), + 'Namespaces should be preserved.' + ); + $this->assertSame( + $namespace . 'Some\\Namespaced\\function_with_leading_slashes', + Runkit::makeNamespaced('\\Some\\Namespaced\\function_with_leading_slashes'), + 'Leading slashes should be stripped.' + ); + } +} diff --git a/tests/Support/functions.php b/tests/stubs/functions.php similarity index 90% rename from tests/Support/functions.php rename to tests/stubs/functions.php index 201485a..a735ce6 100644 --- a/tests/Support/functions.php +++ b/tests/stubs/functions.php @@ -4,7 +4,7 @@ * Dummy functions for the sake of testing. */ -namespace Tests\Support; +namespace Tests\Stubs; /** * Return the sum of three numbers. From 510b10ee6435254754c60a78bf939712b8ed22b9 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Sun, 22 Nov 2020 17:12:54 -0500 Subject: [PATCH 3/3] Update Composer dependencies --- composer.json | 2 +- composer.lock | 31 +++++++++++++------------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index c0f3d02..73428b3 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^0.12", "squizlabs/php_codesniffer": "^3.5", - "stevegrunwell/runkit7-installer": "^1.1", + "stevegrunwell/runkit7-installer": "^1.2", "symfony/phpunit-bridge": "^5.1" }, "suggest": { diff --git a/composer.lock b/composer.lock index 289b97e..03ac9ee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "efac7541b4b02889b60a3582c4bfa3eb", + "content-hash": "79ef5654d4d0319b3e09740bea89b2f4", "packages": [], "packages-dev": [ { @@ -141,16 +141,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.52", + "version": "0.12.57", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e96dd5e7ae9aefed663bc7e285ad96792b67eadc" + "reference": "f9909d1d0c44b4cbaf72babcf80e8f14d6fdd55b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e96dd5e7ae9aefed663bc7e285ad96792b67eadc", - "reference": "e96dd5e7ae9aefed663bc7e285ad96792b67eadc", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9909d1d0c44b4cbaf72babcf80e8f14d6fdd55b", + "reference": "f9909d1d0c44b4cbaf72babcf80e8f14d6fdd55b", "shasum": "" }, "require": { @@ -181,7 +181,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.52" + "source": "https://github.com/phpstan/phpstan/tree/0.12.57" }, "funding": [ { @@ -197,7 +197,7 @@ "type": "tidelift" } ], - "time": "2020-10-25T07:23:44+00:00" + "time": "2020-11-21T12:53:28+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -257,22 +257,18 @@ }, { "name": "stevegrunwell/runkit7-installer", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/stevegrunwell/runkit7-installer.git", - "reference": "76c4cdaf6a03298a545012f1b8d2588c34a10d0e" + "reference": "7a2ed7adc0a0e1e904b94e40c2224101aaf28a16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stevegrunwell/runkit7-installer/zipball/76c4cdaf6a03298a545012f1b8d2588c34a10d0e", - "reference": "76c4cdaf6a03298a545012f1b8d2588c34a10d0e", + "url": "https://api.github.com/repos/stevegrunwell/runkit7-installer/zipball/7a2ed7adc0a0e1e904b94e40c2224101aaf28a16", + "reference": "7a2ed7adc0a0e1e904b94e40c2224101aaf28a16", "shasum": "" }, - "require-dev": { - "php": "^7.1", - "phpunit/phpunit": ">=6.0" - }, "bin": [ "bin/install-runkit.sh" ], @@ -284,11 +280,10 @@ "authors": [ { "name": "Steve Grunwell", - "email": "steve@stevegrunwell.com", "homepage": "https://stevegrunwell.com" } ], - "description": "Installer for PHP Runkit7", + "description": "Installer for PHP's runkit and runkit7 extensions", "keywords": [ "runkit", "testing" @@ -297,7 +292,7 @@ "issues": "https://github.com/stevegrunwell/runkit7-installer/issues", "source": "https://github.com/stevegrunwell/runkit7-installer" }, - "time": "2018-12-05T19:16:14+00:00" + "time": "2020-11-22T22:09:52+00:00" }, { "name": "symfony/phpunit-bridge",