ChangeLog-7.0.md 0000666 00000006076 13436751637 0007253 0 ustar 00 # Changes in PHPUnit 7.0
All notable changes of the PHPUnit 7.0 release series are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
## [7.0.3] - 2018-03-26
* Fixed [#3028](https://github.com/sebastianbergmann/phpunit/pull/3028): TestDox name prettifier does not handle test case classes correctly that are in a `Tests\*` namespace
## [7.0.2] - 2018-02-26
### Fixed
* Fixed [#2974](https://github.com/sebastianbergmann/phpunit/issues/2974): JUnit XML logfile contains invalid characters when test output contains binary data
* Fixed [#3014](https://github.com/sebastianbergmann/phpunit/issues/3014): `TypeError` in `PHPUnit\Framework\TestCase::getActualOutput()` when callback registered using `setOutputCallback()` does not return a string
* Removed more superfluous `@throws \Exception` annotations
## [7.0.1] - 2018-02-13
### Fixed
* Fixed [#3000](https://github.com/sebastianbergmann/phpunit/issues/3000): Directories are not created recursively
* Removed superfluous `@throws \Exception` annotations from assertion methods
## [7.0.0] - 2018-02-02
### Added
* Implemented [#2967](https://github.com/sebastianbergmann/phpunit/pull/2967): Added support for PHP configuration settings to `@requires` annotation
### Changed
* Implemented [#2566](https://github.com/sebastianbergmann/phpunit/issues/2566): Use `Throwable` instead of `Exception` in `PHPUnit\Framework\TestListener` method signatures
* Implemented [#2920](https://github.com/sebastianbergmann/phpunit/pull/2920): Replace CLI TestDox printer with `rpkamp/fancy-testdox-printer`
* Scalar Type Declarations and Return Type Declarations are now used where possible (as a result, the API of `PHPUnit\Framework\TestListener`, for instance, has changed)
* Some classes are now `final`
* The visibility of some methods has been changed from `protected` to `private`
### Removed
* Implemented [#2473](https://github.com/sebastianbergmann/phpunit/issues/2473): Drop support for PHP 7.0
* `@scenario` is no longer an alias for `@test`
* The `PHPUnit\Framework\BaseTestListener` class has been removed (deprecated in PHPUnit 6.4)
* The `PHPUnit\Framework\TestCase::prepareTemplate` template method has been removed
### Fixed
* Fixed [#2169](https://github.com/sebastianbergmann/phpunit/issues/2169): `assertSame()` does not show differences when used on two arrays that are not identical
* Fixed [#2902](https://github.com/sebastianbergmann/phpunit/issues/2902): `@test` annotation gets accepted no matter what
* Fixed [#2907](https://github.com/sebastianbergmann/phpunit/issues/2907): `StringMatchesFormatDescription` constraint does not handle escaped `%` correctly
* Fixed [#2919](https://github.com/sebastianbergmann/phpunit/issues/2919): `assertJsonStringEqualsJsonString()` matches empty object as empty array
[7.0.3]: https://github.com/sebastianbergmann/phpunit/compare/7.0.2...7.0.3
[7.0.2]: https://github.com/sebastianbergmann/phpunit/compare/7.0.1...7.0.2
[7.0.1]: https://github.com/sebastianbergmann/phpunit/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/sebastianbergmann/phpunit/compare/6.5...7.0.0
phpunit.xsd 0000666 00000040112 13436751637 0006774 0 ustar 00
This Schema file defines the rules by which the XML configuration file of PHPUnit 7.4 may be structured.
Root Element
The main type specifying the document structure
LICENSE 0000666 00000003006 13436751637 0005573 0 ustar 00 PHPUnit
Copyright (c) 2001-2018, Sebastian Bergmann .
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Sebastian Bergmann nor the names of his
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
README.md 0000666 00000004604 13436751637 0006052 0 ustar 00 # PHPUnit
PHPUnit is a programmer-oriented testing framework for PHP. It is an instance of the xUnit architecture for unit testing frameworks.
[![Latest Stable Version](https://img.shields.io/packagist/v/phpunit/phpunit.svg?style=flat-square)](https://packagist.org/packages/phpunit/phpunit)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.1-8892BF.svg?style=flat-square)](https://php.net/)
[![Build Status](https://img.shields.io/travis/sebastianbergmann/phpunit/7.4.svg?style=flat-square)](https://phpunit.de/build-status.html)
## Installation
We distribute a [PHP Archive (PHAR)](https://php.net/phar) that has all required (as well as some optional) dependencies of PHPUnit 7.4 bundled in a single file:
```bash
$ wget https://phar.phpunit.de/phpunit-7.4.phar
$ php phpunit-7.4.phar --version
```
Alternatively, you may use [Composer](https://getcomposer.org/) to download and install PHPUnit as well as its dependencies. Please refer to the "[Getting Started](https://phpunit.de/getting-started-with-phpunit.html)" guide for details on how to install PHPUnit.
## Contribute
Please refer to [CONTRIBUTING.md](https://github.com/sebastianbergmann/phpunit/blob/master/.github/CONTRIBUTING.md) for information on how to contribute to PHPUnit and its related projects.
## List of Contributors
Thanks to everyone who has contributed to PHPUnit! You can find a detailed list of contributors on every PHPUnit related package on GitHub. This list shows only the major components:
* [PHPUnit](https://github.com/sebastianbergmann/phpunit/graphs/contributors)
* [php-code-coverage](https://github.com/sebastianbergmann/php-code-coverage/graphs/contributors)
A very special thanks to everyone who has contributed to the documentation and helps maintain the translations:
* [English](https://github.com/sebastianbergmann/phpunit-documentation-english/graphs/contributors)
* [Spanish](https://github.com/sebastianbergmann/phpunit-documentation-spanish/graphs/contributors)
* [French](https://github.com/sebastianbergmann/phpunit-documentation-french/graphs/contributors)
* [Japanese](https://github.com/sebastianbergmann/phpunit-documentation-japanese/graphs/contributors)
* [Brazilian Portuguese](https://github.com/sebastianbergmann/phpunit-documentation-brazilian-portuguese/graphs/contributors)
* [Simplified Chinese](https://github.com/sebastianbergmann/phpunit-documentation-chinese/graphs/contributors)
phpunit 0000666 00000002600 13436751637 0006177 0 ustar 00 #!/usr/bin/env php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (version_compare('7.1.0', PHP_VERSION, '>')) {
fwrite(
STDERR,
sprintf(
'This version of PHPUnit is supported on PHP 7.1 and PHP 7.2.' . PHP_EOL .
'You are using PHP %s (%s).' . PHP_EOL,
PHP_VERSION,
PHP_BINARY
)
);
die(1);
}
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) {
if (file_exists($file)) {
define('PHPUNIT_COMPOSER_INSTALL', $file);
break;
}
}
unset($file);
if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
fwrite(
STDERR,
'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
' composer install' . PHP_EOL . PHP_EOL .
'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
);
die(1);
}
$options = getopt('', array('prepend:'));
if (isset($options['prepend'])) {
require $options['prepend'];
}
unset($options);
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
ChangeLog-7.1.md 0000666 00000005362 13436751637 0007251 0 ustar 00 # Changes in PHPUnit 7.1
All notable changes of the PHPUnit 7.1 release series are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [7.1.6] - 2018-MM-DD
### Fixed
* Fixed [#3107](https://github.com/sebastianbergmann/phpunit/issues/3107): `CliTestDoxPrinter::addError()` cannot handle errors in `setUpBeforeClass()`
* Fixed [#3142](https://github.com/sebastianbergmann/phpunit/issues/3142): Method-level annotations (`@backupGlobals`, `@backupStaticAttributes`, `@errorHandler`, `@preserveGlobalState`) do not override class-level annotations
## [7.1.5] - 2018-04-29
### Fixed
* Fixed [#3105](https://github.com/sebastianbergmann/phpunit/pull/3105): Name is prettified inconsistently when snake_case notation is used
## [7.1.4] - 2018-04-18
### Fixed
* Fixed [#3034](https://github.com/sebastianbergmann/phpunit/pull/3034): `$this->getStatus()` returns `STATUS_PASSED` in `tearDown()` after unexpected exception
## [7.1.3] - 2018-04-13
### Fixed
* Fixed [#3094](https://github.com/sebastianbergmann/phpunit/issues/3094): Faulty dependency constraint affecting `getObjectForTrait()` (failure using `--prefer-lowest`)
## [7.1.2] - 2018-04-10
### Fixed
* Fixed [#2830](https://github.com/sebastianbergmann/phpunit/issues/2830): `@runClassInSeparateProcess` does not work for tests that use `@dataProvider`
* Fixed [#3059](https://github.com/sebastianbergmann/phpunit/pull/3059): `StringMatchesFormatDescription` constraint fails when matching multiline with `\r\n`
* Fixed [#3087](https://github.com/sebastianbergmann/phpunit/pull/3087): `TestCase::getTestResultObject()` can return `null`
## [7.1.1] - 2018-04-06
### Fixed
* `CliTestDoxPrinter::writeProgress()` and `TeamCity::writeProgress()` are not compatible with `ResultPrinter::writeProgress()` (on PHP 7.1)
## [7.1.0] - 2018-04-06
### Added
* Implemented [#3002](https://github.com/sebastianbergmann/phpunit/issues/3002): Support for test runner extensions
* Implemented [#3035](https://github.com/sebastianbergmann/phpunit/pull/3035): Add support for `iterable` in `assertInternalType()`
### Changed
* `PHPUnit\Framework\Assert` is no longer searched for test methods
* `ReflectionMethod::invokeArgs()` is no longer used to invoke test methods
[7.1.6]: https://github.com/sebastianbergmann/phpunit/compare/7.1.5...7.1.6
[7.1.5]: https://github.com/sebastianbergmann/phpunit/compare/7.1.4...7.1.5
[7.1.4]: https://github.com/sebastianbergmann/phpunit/compare/7.1.3...7.1.4
[7.1.3]: https://github.com/sebastianbergmann/phpunit/compare/7.1.2...7.1.3
[7.1.2]: https://github.com/sebastianbergmann/phpunit/compare/7.1.1...7.1.2
[7.1.1]: https://github.com/sebastianbergmann/phpunit/compare/7.1.0...7.1.1
[7.1.0]: https://github.com/sebastianbergmann/phpunit/compare/7.0...7.1.0
.github/ISSUE_TEMPLATE.md 0000666 00000001145 13436751637 0010635 0 ustar 00 | Q | A
| --------------------| ---------------
| PHPUnit version | x.y.z
| PHP version | x.y.z
| Installation Method | Composer / PHAR
.github/stale.yml 0000666 00000003074 13436751637 0007766 0 ustar 00 # Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- blocked
- enhancement
- backward-compatibility-break
- feature-removal
- php-support-removal
- process
- rfc
- refactoring
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had activity within the last 60 days. It will be closed after 7 days if no further activity occurs. Thank you for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been automatically closed because it has not had activity since it was marked as stale. Thank you for your contributions.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
.github/CODE_OF_CONDUCT.md 0000666 00000004532 13436751637 0010732 0 ustar 00 # Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at sebastian@phpunit.de. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/
.github/CONTRIBUTING.md 0000666 00000004726 13436751637 0010371 0 ustar 00 # Contributing to PHPUnit
## Contributor Code of Conduct
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
## Workflow
* Fork the project.
* Make your bug fix or feature addition.
* Add tests for it. This is important so we don't break it in a future version unintentionally.
* Send a pull request. Bonus points for topic branches.
Please make sure that you have [set up your user name and email address](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup) for use with Git. Strings such as `silly nick name ` look really stupid in the commit history of a project.
Pull requests for bug fixes must be based on the current stable branch whereas pull requests for new features must be based on the `master` branch.
We are trying to keep backwards compatibility breaks in PHPUnit to an absolute minimum. Please take this into account when proposing changes.
Due to time constraints, we are not always able to respond as quickly as we would like. Please do not take delays personal and feel free to remind us if you feel that we forgot to respond.
## Coding Guidelines
This project comes with a configuration file and an executable for [php-cs-fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) (`.php_cs`) that you can use to (re)format your source code for compliance with this project's coding guidelines:
```bash
$ ./build/tools/php-cs-fixer fix
```
## Using PHPUnit from a Git checkout
The following commands can be used to perform the initial checkout of PHPUnit:
```bash
$ git clone git://github.com/sebastianbergmann/phpunit.git
$ cd phpunit
```
Retrieve PHPUnit's dependencies using [Composer](https://getcomposer.org/):
```bash
$ composer install
```
The `phpunit` script can be used to invoke the PHPUnit test runner:
```bash
$ ./phpunit --version
```
## Running PHPUnit's own test suite
After following the steps shown above, PHPUnit's own test suite is run like this:
```bash
$ ./phpunit
```
## Reporting issues
Please use the most specific issue tracker to search for existing tickets and to open new tickets:
* [General problems](https://github.com/sebastianbergmann/phpunit/issues)
* [Code Coverage](https://github.com/sebastianbergmann/php-code-coverage/issues)
* [Documentation](https://github.com/sebastianbergmann/phpunit-documentation-english/issues)
* [Website](https://github.com/sebastianbergmann/phpunit-website/issues)
ChangeLog-6.5.md 0000666 00000012534 13436751637 0007253 0 ustar 00 # Changes in PHPUnit 6.5
All notable changes of the PHPUnit 6.5 release series are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [6.5.13] - 2018-09-08
* Fixed [#3181](https://github.com/sebastianbergmann/phpunit/issues/3181): `--filter` should be case-insensitive
* Fixed [#3234](https://github.com/sebastianbergmann/phpunit/issues/3234): `assertArraySubset()` with `$strict=true` does not display differences properly
* Fixed [#3254](https://github.com/sebastianbergmann/phpunit/issues/3254): TextUI test runner cannot run a `Test` instance that is not a `TestSuite`
## [6.5.12] - 2018-08-22
* Fixed [#3248](https://github.com/sebastianbergmann/phpunit/issues/3248) and [#3233](https://github.com/sebastianbergmann/phpunit/issues/3233): `phpunit.xsd` dictates element order where it should not
* Fixed [#3251](https://github.com/sebastianbergmann/phpunit/issues/3251): TeamCity result logger is missing test duration information
## [6.5.11] - 2018-08-07
* Fixed [#3219](https://github.com/sebastianbergmann/phpunit/issues/3219): `getMockFromWsdl()` generates invalid PHP code when WSDL filename contains special characters
## [6.5.10] - 2018-08-03
### Fixed
* Fixed [#3209](https://github.com/sebastianbergmann/phpunit/issues/3209): `Test::run()` and `TestCase::run()` interface contradiction
* Fixed [#3218](https://github.com/sebastianbergmann/phpunit/issues/3218): `prefix` attribute for `directory` node missing from `phpunit.xml` XSD
* Fixed [#3225](https://github.com/sebastianbergmann/phpunit/issues/3225): `coverage-php` missing from `phpunit.xsd`
## [6.5.9] - 2018-07-03
### Fixed
* Fixed [#3142](https://github.com/sebastianbergmann/phpunit/issues/3142): Method-level annotations (`@backupGlobals`, `@backupStaticAttributes`, `@errorHandler`, `@preserveGlobalState`) do not override class-level annotations
## [6.5.8] - 2018-04-10
### Fixed
* Fixed [#2830](https://github.com/sebastianbergmann/phpunit/issues/2830): `@runClassInSeparateProcess` does not work for tests that use `@dataProvider`
## [6.5.7] - 2018-02-26
### Fixed
* Fixed [#2974](https://github.com/sebastianbergmann/phpunit/issues/2974): JUnit XML logfile contains invalid characters when test output contains binary data
## [6.5.6] - 2018-02-01
### Fixed
* Fixed [#2236](https://github.com/sebastianbergmann/phpunit/issues/2236): Exceptions in `tearDown()` do not affect `getStatus()`
* Fixed [#2950](https://github.com/sebastianbergmann/phpunit/issues/2950): Class extending `PHPUnit\Framework\TestSuite` does not extend `PHPUnit\FrameworkTestCase`
* Fixed [#2972](https://github.com/sebastianbergmann/phpunit/issues/2972): PHPUnit crashes when test suite contains both `.phpt` files and unconventionally named tests
## [6.5.5] - 2017-12-17
### Fixed
* Fixed [#2922](https://github.com/sebastianbergmann/phpunit/issues/2922): Test class is not discovered when there is a test class with `@group` and provider throwing exception in it, tests are run with `--exclude-group` for that group, there is another class called later (after the class from above), and the name of that another class does not match its filename
## [6.5.4] - 2017-12-10
### Changed
* Require version 5.0.5 of `phpunit/phpunit-mock-objects` for [phpunit-mock-objects#394](https://github.com/sebastianbergmann/phpunit-mock-objects/issues/394)
## [6.5.3] - 2017-12-06
### Fixed
* Fixed an issue with PHPT tests when `forceCoversAnnotation="true"` is configured
## [6.5.2] - 2017-12-02
### Changed
* Require version 5.0.4 of `phpunit/phpunit-mock-objects` for [phpunit-mock-objects#388](https://github.com/sebastianbergmann/phpunit-mock-objects/issues/388)
## [6.5.1] - 2017-12-01
* Fixed [#2886](https://github.com/sebastianbergmann/phpunit/pull/2886): Forced environment variables do not affect `getenv()`
## [6.5.0] - 2017-12-01
### Added
* Implemented [#2286](https://github.com/sebastianbergmann/phpunit/issues/2286): Optional `$exit` parameter for `PHPUnit\TextUI\TestRunner::run()`
* Implemented [#2496](https://github.com/sebastianbergmann/phpunit/issues/2496): Allow shallow copy of dependencies
### Fixed
* Fixed [#2654](https://github.com/sebastianbergmann/phpunit/issues/2654): Problems with `assertJsonStringEqualsJsonString()`
* Fixed [#2810](https://github.com/sebastianbergmann/phpunit/pull/2810): Code Coverage for PHPT tests does not work
[6.5.13]: https://github.com/sebastianbergmann/phpunit/compare/6.5.12...6.5.13
[6.5.12]: https://github.com/sebastianbergmann/phpunit/compare/6.5.11...6.5.12
[6.5.11]: https://github.com/sebastianbergmann/phpunit/compare/6.5.10...6.5.11
[6.5.10]: https://github.com/sebastianbergmann/phpunit/compare/6.5.9...6.5.10
[6.5.9]: https://github.com/sebastianbergmann/phpunit/compare/6.5.8...6.5.9
[6.5.8]: https://github.com/sebastianbergmann/phpunit/compare/6.5.7...6.5.8
[6.5.7]: https://github.com/sebastianbergmann/phpunit/compare/6.5.6...6.5.7
[6.5.6]: https://github.com/sebastianbergmann/phpunit/compare/6.5.5...6.5.6
[6.5.5]: https://github.com/sebastianbergmann/phpunit/compare/6.5.4...6.5.5
[6.5.4]: https://github.com/sebastianbergmann/phpunit/compare/6.5.3...6.5.4
[6.5.3]: https://github.com/sebastianbergmann/phpunit/compare/6.5.2...6.5.3
[6.5.2]: https://github.com/sebastianbergmann/phpunit/compare/6.5.1...6.5.2
[6.5.1]: https://github.com/sebastianbergmann/phpunit/compare/6.5.0...6.5.1
[6.5.0]: https://github.com/sebastianbergmann/phpunit/compare/6.4...6.5.0
phpunit.xml 0000666 00000001662 13436751637 0007005 0 ustar 00
tests/unit
tests/end-to-end
src
src/Framework/Assert/Functions.php
src/Util/PHP/eval-stdin.php
ChangeLog-7.2.md 0000666 00000007424 13436751637 0007253 0 ustar 00 # Changes in PHPUnit 7.2
All notable changes of the PHPUnit 7.2 release series are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [7.2.7] - 2018-07-15
### Fixed
* Fixed [#3154](https://github.com/sebastianbergmann/phpunit/issues/3154): Global constants as default parameter values are not handled correctly in namespace
* Fixed [#3189](https://github.com/sebastianbergmann/phpunit/issues/3189): PHPUnit 7.2 potentially leaves a messy libxmlerror state
* Fixed [#3199](https://github.com/sebastianbergmann/phpunit/pull/3199): Code Coverage for PHPT tests does not work when PHPDBG is used
## [7.2.6] - 2018-06-21
### Fixed
* Fixed [#3176](https://github.com/sebastianbergmann/phpunit/issues/3176): PHPUnit 7.2.5 breaks backward compatibility
## [7.2.5] - 2018-06-21
### Fixed
* Fixed [#3093](https://github.com/sebastianbergmann/phpunit/issues/3093): Unable to chain a `@dataProvider` in method `a` with a `@depends` in method `b`
* Fixed [#3174](https://github.com/sebastianbergmann/phpunit/issues/3174): Code generator for test doubles does not handle proxied methods with variadic parameters correctly
## [7.2.4] - 2018-06-05
### Fixed
* Fixed [#3160](https://github.com/sebastianbergmann/phpunit/issues/3160): TeamCity logfile writer broken on Windows
## [7.2.3] - 2018-06-03
### Fixed
* Fixed [#3156](https://github.com/sebastianbergmann/phpunit/issues/3156): Combined use of `@depends` and `@dataProvider` is not handled correctly
## [7.2.2] - 2018-06-01
### Changed
* Ensure that `phpunit/php-code-coverage` is used in version `^6.0.7`
## [7.2.1] - 2018-06-01
### Fixed
* Fixed [#3155](https://github.com/sebastianbergmann/phpunit/issues/3155): Calling `getStatus()` on a `TestCase` object before the respective test has been executed results in type error
## [7.2.0] - 2018-06-01
### Added
* Implemented [#3042](https://github.com/sebastianbergmann/phpunit/pull/3042): Add `TestCase::expectNotToPerformAssertions()` method as alternative to `@doesNotPerformAssertions` annotation
* Implemented [#3064](https://github.com/sebastianbergmann/phpunit/issues/3064): Mark tests as risky when they claim not to perform assertions but do
* Implemented [#3066](https://github.com/sebastianbergmann/phpunit/issues/3066): Validate XML configuration against XSD
* Implemented [#3076](https://github.com/sebastianbergmann/phpunit/issues/3076): Extensions can be configured via PHPUnit's XML configuration
* Implemented [#3080](https://github.com/sebastianbergmann/phpunit/issues/3080): The XML configuration arguments can have boolean elements
* Implemented [#3092](https://github.com/sebastianbergmann/phpunit/pull/3092): Ability to run tests in random order, reverse order, ordered using dependency resolution
### Changed
* Implemented [#3103](https://github.com/sebastianbergmann/phpunit/issues/3103): Merge `phpunit-mock-objects` back into PHPUnit's Git repository
* Implemented [#3115](https://github.com/sebastianbergmann/phpunit/pull/3115): Method-level `@covers` annotation overrides class-level `@coversNothing` annotation
### Removed
* Fixed [#3069](https://github.com/sebastianbergmann/phpunit/issues/3069): Method `ResultPrinter::printWaitPrompt()` seems to be unused
[7.2.7]: https://github.com/sebastianbergmann/phpunit/compare/7.2.6...7.2.7
[7.2.6]: https://github.com/sebastianbergmann/phpunit/compare/7.2.5...7.2.6
[7.2.5]: https://github.com/sebastianbergmann/phpunit/compare/7.2.4...7.2.5
[7.2.4]: https://github.com/sebastianbergmann/phpunit/compare/7.2.3...7.2.4
[7.2.3]: https://github.com/sebastianbergmann/phpunit/compare/7.2.2...7.2.3
[7.2.2]: https://github.com/sebastianbergmann/phpunit/compare/7.2.1...7.2.2
[7.2.1]: https://github.com/sebastianbergmann/phpunit/compare/7.2.0...7.2.1
[7.2.0]: https://github.com/sebastianbergmann/phpunit/compare/7.1...7.2.0
src/Exception.php 0000666 00000000504 13436751637 0010024 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit;
/**
* Marker interface for PHPUnit exceptions.
*/
interface Exception
{
}
src/Util/PHP/DefaultPhpProcess.php 0000666 00000012377 13436751637 0013040 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\PHP;
use PHPUnit\Framework\Exception;
/**
* Default utility for PHP sub-processes.
*/
class DefaultPhpProcess extends AbstractPhpProcess
{
/**
* @var string
*/
protected $tempFile;
/**
* Runs a single job (PHP code) using a separate PHP process.
*
* @throws Exception
*/
public function runJob(string $job, array $settings = []): array
{
if ($this->useTemporaryFile() || $this->stdin) {
if (!($this->tempFile = \tempnam(\sys_get_temp_dir(), 'PHPUnit')) ||
\file_put_contents($this->tempFile, $job) === false) {
throw new Exception(
'Unable to write temporary file'
);
}
$job = $this->stdin;
}
return $this->runProcess($job, $settings);
}
/**
* Returns an array of file handles to be used in place of pipes
*/
protected function getHandles(): array
{
return [];
}
/**
* Handles creating the child process and returning the STDOUT and STDERR
*
* @throws Exception
*/
protected function runProcess(string $job, array $settings): array
{
$handles = $this->getHandles();
$env = null;
if ($this->env) {
$env = $_SERVER ?? [];
unset($env['argv'], $env['argc']);
$env = \array_merge($env, $this->env);
foreach ($env as $envKey => $envVar) {
if (\is_array($envVar)) {
unset($env[$envKey]);
}
}
}
$pipeSpec = [
0 => $handles[0] ?? ['pipe', 'r'],
1 => $handles[1] ?? ['pipe', 'w'],
2 => $handles[2] ?? ['pipe', 'w'],
];
$process = \proc_open(
$this->getCommand($settings, $this->tempFile),
$pipeSpec,
$pipes,
null,
$env
);
if (!\is_resource($process)) {
throw new Exception(
'Unable to spawn worker process'
);
}
if ($job) {
$this->process($pipes[0], $job);
}
\fclose($pipes[0]);
$stderr = $stdout = '';
if ($this->timeout) {
unset($pipes[0]);
while (true) {
$r = $pipes;
$w = null;
$e = null;
$n = @\stream_select($r, $w, $e, $this->timeout);
if ($n === false) {
break;
}
if ($n === 0) {
\proc_terminate($process, 9);
throw new Exception(
\sprintf(
'Job execution aborted after %d seconds',
$this->timeout
)
);
}
if ($n > 0) {
foreach ($r as $pipe) {
$pipeOffset = 0;
foreach ($pipes as $i => $origPipe) {
if ($pipe === $origPipe) {
$pipeOffset = $i;
break;
}
}
if (!$pipeOffset) {
break;
}
$line = \fread($pipe, 8192);
if ($line === '') {
\fclose($pipes[$pipeOffset]);
unset($pipes[$pipeOffset]);
} else {
if ($pipeOffset === 1) {
$stdout .= $line;
} else {
$stderr .= $line;
}
}
}
if (empty($pipes)) {
break;
}
}
}
} else {
if (isset($pipes[1])) {
$stdout = \stream_get_contents($pipes[1]);
\fclose($pipes[1]);
}
if (isset($pipes[2])) {
$stderr = \stream_get_contents($pipes[2]);
\fclose($pipes[2]);
}
}
if (isset($handles[1])) {
\rewind($handles[1]);
$stdout = \stream_get_contents($handles[1]);
\fclose($handles[1]);
}
if (isset($handles[2])) {
\rewind($handles[2]);
$stderr = \stream_get_contents($handles[2]);
\fclose($handles[2]);
}
\proc_close($process);
$this->cleanup();
return ['stdout' => $stdout, 'stderr' => $stderr];
}
protected function process($pipe, string $job): void
{
\fwrite($pipe, $job);
}
protected function cleanup(): void
{
if ($this->tempFile) {
\unlink($this->tempFile);
}
}
protected function useTemporaryFile(): bool
{
return false;
}
}
src/Util/PHP/AbstractPhpProcess.php 0000666 00000023047 13436751637 0013213 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\PHP;
use __PHP_Incomplete_Class;
use ErrorException;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\SyntheticError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestFailure;
use PHPUnit\Framework\TestResult;
use SebastianBergmann\Environment\Runtime;
/**
* Utility methods for PHP sub-processes.
*/
abstract class AbstractPhpProcess
{
/**
* @var Runtime
*/
protected $runtime;
/**
* @var bool
*/
protected $stderrRedirection = false;
/**
* @var string
*/
protected $stdin = '';
/**
* @var string
*/
protected $args = '';
/**
* @var array
*/
protected $env = [];
/**
* @var int
*/
protected $timeout = 0;
public static function factory(): self
{
if (\DIRECTORY_SEPARATOR === '\\') {
return new WindowsPhpProcess;
}
return new DefaultPhpProcess;
}
public function __construct()
{
$this->runtime = new Runtime;
}
/**
* Defines if should use STDERR redirection or not.
*
* Then $stderrRedirection is TRUE, STDERR is redirected to STDOUT.
*/
public function setUseStderrRedirection(bool $stderrRedirection): void
{
$this->stderrRedirection = $stderrRedirection;
}
/**
* Returns TRUE if uses STDERR redirection or FALSE if not.
*/
public function useStderrRedirection(): bool
{
return $this->stderrRedirection;
}
/**
* Sets the input string to be sent via STDIN
*/
public function setStdin(string $stdin): void
{
$this->stdin = $stdin;
}
/**
* Returns the input string to be sent via STDIN
*/
public function getStdin(): string
{
return $this->stdin;
}
/**
* Sets the string of arguments to pass to the php job
*/
public function setArgs(string $args): void
{
$this->args = $args;
}
/**
* Returns the string of arguments to pass to the php job
*/
public function getArgs(): string
{
return $this->args;
}
/**
* Sets the array of environment variables to start the child process with
*
* @param array $env
*/
public function setEnv(array $env): void
{
$this->env = $env;
}
/**
* Returns the array of environment variables to start the child process with
*/
public function getEnv(): array
{
return $this->env;
}
/**
* Sets the amount of seconds to wait before timing out
*/
public function setTimeout(int $timeout): void
{
$this->timeout = $timeout;
}
/**
* Returns the amount of seconds to wait before timing out
*/
public function getTimeout(): int
{
return $this->timeout;
}
/**
* Runs a single test in a separate PHP process.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function runTestJob(string $job, Test $test, TestResult $result): void
{
$result->startTest($test);
$_result = $this->runJob($job);
$this->processChildResult(
$test,
$result,
$_result['stdout'],
$_result['stderr']
);
}
/**
* Returns the command based into the configurations.
*/
public function getCommand(array $settings, string $file = null): string
{
$command = $this->runtime->getBinary();
$command .= $this->settingsToParameters($settings);
if (\PHP_SAPI === 'phpdbg') {
$command .= ' -qrr';
if (!$file) {
$command .= 's=';
}
}
if ($file) {
$command .= ' ' . \escapeshellarg($file);
}
if ($this->args) {
if (!$file) {
$command .= ' --';
}
$command .= ' ' . $this->args;
}
if ($this->stderrRedirection === true) {
$command .= ' 2>&1';
}
return $command;
}
/**
* Runs a single job (PHP code) using a separate PHP process.
*/
abstract public function runJob(string $job, array $settings = []): array;
protected function settingsToParameters(array $settings): string
{
$buffer = '';
foreach ($settings as $setting) {
$buffer .= ' -d ' . \escapeshellarg($setting);
}
return $buffer;
}
/**
* Processes the TestResult object from an isolated process.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
private function processChildResult(Test $test, TestResult $result, string $stdout, string $stderr): void
{
$time = 0;
if (!empty($stderr)) {
$result->addError(
$test,
new Exception(\trim($stderr)),
$time
);
} else {
\set_error_handler(function ($errno, $errstr, $errfile, $errline): void {
throw new ErrorException($errstr, $errno, $errno, $errfile, $errline);
});
try {
if (\strpos($stdout, "#!/usr/bin/env php\n") === 0) {
$stdout = \substr($stdout, 19);
}
$childResult = \unserialize(\str_replace("#!/usr/bin/env php\n", '', $stdout));
\restore_error_handler();
} catch (ErrorException $e) {
\restore_error_handler();
$childResult = false;
$result->addError(
$test,
new Exception(\trim($stdout), 0, $e),
$time
);
}
if ($childResult !== false) {
if (!empty($childResult['output'])) {
$output = $childResult['output'];
}
/* @var TestCase $test */
$test->setResult($childResult['testResult']);
$test->addToAssertionCount($childResult['numAssertions']);
/** @var TestResult $childResult */
$childResult = $childResult['result'];
if ($result->getCollectCodeCoverageInformation()) {
$result->getCodeCoverage()->merge(
$childResult->getCodeCoverage()
);
}
$time = $childResult->time();
$notImplemented = $childResult->notImplemented();
$risky = $childResult->risky();
$skipped = $childResult->skipped();
$errors = $childResult->errors();
$warnings = $childResult->warnings();
$failures = $childResult->failures();
if (!empty($notImplemented)) {
$result->addError(
$test,
$this->getException($notImplemented[0]),
$time
);
} elseif (!empty($risky)) {
$result->addError(
$test,
$this->getException($risky[0]),
$time
);
} elseif (!empty($skipped)) {
$result->addError(
$test,
$this->getException($skipped[0]),
$time
);
} elseif (!empty($errors)) {
$result->addError(
$test,
$this->getException($errors[0]),
$time
);
} elseif (!empty($warnings)) {
$result->addWarning(
$test,
$this->getException($warnings[0]),
$time
);
} elseif (!empty($failures)) {
$result->addFailure(
$test,
$this->getException($failures[0]),
$time
);
}
}
}
$result->endTest($test, $time);
if (!empty($output)) {
print $output;
}
}
/**
* Gets the thrown exception from a PHPUnit\Framework\TestFailure.
*
* @see https://github.com/sebastianbergmann/phpunit/issues/74
*/
private function getException(TestFailure $error): Exception
{
$exception = $error->thrownException();
if ($exception instanceof __PHP_Incomplete_Class) {
$exceptionArray = [];
foreach ((array) $exception as $key => $value) {
$key = \substr($key, \strrpos($key, "\0") + 1);
$exceptionArray[$key] = $value;
}
$exception = new SyntheticError(
\sprintf(
'%s: %s',
$exceptionArray['_PHP_Incomplete_Class_Name'],
$exceptionArray['message']
),
$exceptionArray['code'],
$exceptionArray['file'],
$exceptionArray['line'],
$exceptionArray['trace']
);
}
return $exception;
}
}
src/Util/PHP/eval-stdin.php 0000666 00000000424 13436751637 0011501 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
eval('?>' . \file_get_contents('php://stdin'));
src/Util/PHP/WindowsPhpProcess.php 0000666 00000002131 13436751637 0013071 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\PHP;
use PHPUnit\Framework\Exception;
/**
* Windows utility for PHP sub-processes.
*
* Reading from STDOUT or STDERR hangs forever on Windows if the output is
* too large.
*
* @see https://bugs.php.net/bug.php?id=51800
*/
class WindowsPhpProcess extends DefaultPhpProcess
{
public function getCommand(array $settings, string $file = null): string
{
return '"' . parent::getCommand($settings, $file) . '"';
}
protected function getHandles(): array
{
if (false === $stdout_handle = \tmpfile()) {
throw new Exception(
'A temporary file could not be created; verify that your TEMP environment variable is writable'
);
}
return [
1 => $stdout_handle,
];
}
protected function useTemporaryFile(): bool
{
return true;
}
}
src/Util/PHP/Template/TestCaseClass.tpl.dist 0000666 00000006023 13436751637 0014662 0 ustar 00 setCodeCoverage(
new CodeCoverage(
null,
unserialize('{codeCoverageFilter}')
)
);
}
$result->beStrictAboutTestsThatDoNotTestAnything({isStrictAboutTestsThatDoNotTestAnything});
$result->beStrictAboutOutputDuringTests({isStrictAboutOutputDuringTests});
$result->enforceTimeLimit({enforcesTimeLimit});
$result->beStrictAboutTodoAnnotatedTests({isStrictAboutTodoAnnotatedTests});
$result->beStrictAboutResourceUsageDuringSmallTests({isStrictAboutResourceUsageDuringSmallTests});
$test = new {className}('{name}', unserialize('{data}'), '{dataName}');
$test->setDependencyInput(unserialize('{dependencyInput}'));
$test->setInIsolation(TRUE);
ob_end_clean();
$test->run($result);
$output = '';
if (!$test->hasExpectationOnOutput()) {
$output = $test->getActualOutput();
}
ini_set('xdebug.scream', 0);
@rewind(STDOUT); /* @ as not every STDOUT target stream is rewindable */
if ($stdout = stream_get_contents(STDOUT)) {
$output = $stdout . $output;
$streamMetaData = stream_get_meta_data(STDOUT);
if (!empty($streamMetaData['stream_type']) && 'STDIO' === $streamMetaData['stream_type']) {
@ftruncate(STDOUT, 0);
@rewind(STDOUT);
}
}
print serialize(
[
'testResult' => $test->getResult(),
'numAssertions' => $test->getNumAssertions(),
'result' => $result,
'output' => $output
]
);
}
$configurationFilePath = '{configurationFilePath}';
if ('' !== $configurationFilePath) {
$configuration = PHPUnit\Util\Configuration::getInstance($configurationFilePath);
$configuration->handlePHPConfiguration();
unset($configuration);
}
function __phpunit_error_handler($errno, $errstr, $errfile, $errline, $errcontext)
{
return true;
}
set_error_handler('__phpunit_error_handler');
{constants}
{included_files}
{globals}
restore_error_handler();
if (isset($GLOBALS['__PHPUNIT_BOOTSTRAP'])) {
require_once $GLOBALS['__PHPUNIT_BOOTSTRAP'];
unset($GLOBALS['__PHPUNIT_BOOTSTRAP']);
}
__phpunit_run_isolated_test();
src/Util/PHP/Template/PhptTestCase.tpl.dist 0000666 00000001542 13436751637 0014531 0 ustar 00 start(__FILE__);
}
register_shutdown_function(function() use ($coverage) {
$output = null;
if ($coverage) {
$output = $coverage->stop();
}
file_put_contents('{coverageFile}', serialize($output));
});
ob_end_clean();
require '{job}';
src/Util/PHP/Template/TestCaseMethod.tpl.dist 0000666 00000006132 13436751637 0015036 0 ustar 00 setCodeCoverage(
new CodeCoverage(
null,
unserialize('{codeCoverageFilter}')
)
);
}
$result->beStrictAboutTestsThatDoNotTestAnything({isStrictAboutTestsThatDoNotTestAnything});
$result->beStrictAboutOutputDuringTests({isStrictAboutOutputDuringTests});
$result->enforceTimeLimit({enforcesTimeLimit});
$result->beStrictAboutTodoAnnotatedTests({isStrictAboutTodoAnnotatedTests});
$result->beStrictAboutResourceUsageDuringSmallTests({isStrictAboutResourceUsageDuringSmallTests});
/** @var TestCase $test */
$test = new {className}('{methodName}', unserialize('{data}'), '{dataName}');
$test->setDependencyInput(unserialize('{dependencyInput}'));
$test->setInIsolation(TRUE);
ob_end_clean();
$test->run($result);
$output = '';
if (!$test->hasExpectationOnOutput()) {
$output = $test->getActualOutput();
}
ini_set('xdebug.scream', '0');
@rewind(STDOUT); /* @ as not every STDOUT target stream is rewindable */
if ($stdout = stream_get_contents(STDOUT)) {
$output = $stdout . $output;
$streamMetaData = stream_get_meta_data(STDOUT);
if (!empty($streamMetaData['stream_type']) && 'STDIO' === $streamMetaData['stream_type']) {
@ftruncate(STDOUT, 0);
@rewind(STDOUT);
}
}
print serialize(
[
'testResult' => $test->getResult(),
'numAssertions' => $test->getNumAssertions(),
'result' => $result,
'output' => $output
]
);
}
$configurationFilePath = '{configurationFilePath}';
if ('' !== $configurationFilePath) {
$configuration = PHPUnit\Util\Configuration::getInstance($configurationFilePath);
$configuration->handlePHPConfiguration();
unset($configuration);
}
function __phpunit_error_handler($errno, $errstr, $errfile, $errline, $errcontext)
{
return true;
}
set_error_handler('__phpunit_error_handler');
{constants}
{included_files}
{globals}
restore_error_handler();
if (isset($GLOBALS['__PHPUNIT_BOOTSTRAP'])) {
require_once $GLOBALS['__PHPUNIT_BOOTSTRAP'];
unset($GLOBALS['__PHPUNIT_BOOTSTRAP']);
}
__phpunit_run_isolated_test();
src/Util/Blacklist.php 0000666 00000007231 13436751637 0010717 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use Composer\Autoload\ClassLoader;
use DeepCopy\DeepCopy;
use Doctrine\Instantiator\Instantiator;
use PHP_Token;
use phpDocumentor\Reflection\DocBlock;
use PHPUnit\Framework\MockObject\Generator;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophet;
use ReflectionClass;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Diff\Diff;
use SebastianBergmann\Environment\Runtime;
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
use SebastianBergmann\GlobalState\Snapshot;
use SebastianBergmann\Invoker\Invoker;
use SebastianBergmann\RecursionContext\Context;
use SebastianBergmann\Timer\Timer;
use SebastianBergmann\Version;
use Text_Template;
/**
* Utility class for blacklisting PHPUnit's own source code files.
*/
final class Blacklist
{
/**
* @var array
*/
public static $blacklistedClassNames = [
FileIteratorFacade::class => 1,
Timer::class => 1,
PHP_Token::class => 1,
TestCase::class => 2,
'PHPUnit\DbUnit\TestCase' => 2,
Generator::class => 1,
Text_Template::class => 1,
'Symfony\Component\Yaml\Yaml' => 1,
CodeCoverage::class => 1,
Diff::class => 1,
Runtime::class => 1,
Comparator::class => 1,
Exporter::class => 1,
Snapshot::class => 1,
Invoker::class => 1,
Context::class => 1,
Version::class => 1,
ClassLoader::class => 1,
Instantiator::class => 1,
DocBlock::class => 1,
Prophet::class => 1,
DeepCopy::class => 1,
];
/**
* @var string[]
*/
private static $directories;
/**
* @return string[]
*/
public function getBlacklistedDirectories(): array
{
$this->initialize();
return self::$directories;
}
public function isBlacklisted(string $file): bool
{
if (\defined('PHPUNIT_TESTSUITE')) {
return false;
}
$this->initialize();
foreach (self::$directories as $directory) {
if (\strpos($file, $directory) === 0) {
return true;
}
}
return false;
}
private function initialize(): void
{
if (self::$directories === null) {
self::$directories = [];
foreach (self::$blacklistedClassNames as $className => $parent) {
if (!\class_exists($className)) {
continue;
}
$reflector = new ReflectionClass($className);
$directory = $reflector->getFileName();
for ($i = 0; $i < $parent; $i++) {
$directory = \dirname($directory);
}
self::$directories[] = $directory;
}
// Hide process isolation workaround on Windows.
if (\DIRECTORY_SEPARATOR === '\\') {
// tempnam() prefix is limited to first 3 chars.
// @see https://php.net/manual/en/function.tempnam.php
self::$directories[] = \sys_get_temp_dir() . '\\PHP';
}
}
}
}
src/Util/Xml.php 0000666 00000017630 13436751637 0007553 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use DOMCharacterData;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
use PHPUnit\Framework\Exception;
use ReflectionClass;
final class Xml
{
public static function import(DOMElement $element): DOMElement
{
$document = new DOMDocument;
return $document->importNode($element, true);
}
/**
* Load an $actual document into a DOMDocument. This is called
* from the selector assertions.
*
* If $actual is already a DOMDocument, it is returned with
* no changes. Otherwise, $actual is loaded into a new DOMDocument
* as either HTML or XML, depending on the value of $isHtml. If $isHtml is
* false and $xinclude is true, xinclude is performed on the loaded
* DOMDocument.
*
* Note: prior to PHPUnit 3.3.0, this method loaded a file and
* not a string as it currently does. To load a file into a
* DOMDocument, use loadFile() instead.
*
* @param DOMDocument|string $actual
*
* @throws Exception
*/
public static function load($actual, bool $isHtml = false, string $filename = '', bool $xinclude = false, bool $strict = false): DOMDocument
{
if ($actual instanceof DOMDocument) {
return $actual;
}
if (!\is_string($actual)) {
throw new Exception('Could not load XML from ' . \gettype($actual));
}
if ($actual === '') {
throw new Exception('Could not load XML from empty string');
}
// Required for XInclude on Windows.
if ($xinclude) {
$cwd = \getcwd();
@\chdir(\dirname($filename));
}
$document = new DOMDocument;
$document->preserveWhiteSpace = false;
$internal = \libxml_use_internal_errors(true);
$message = '';
$reporting = \error_reporting(0);
if ($filename !== '') {
// Required for XInclude
$document->documentURI = $filename;
}
if ($isHtml) {
$loaded = $document->loadHTML($actual);
} else {
$loaded = $document->loadXML($actual);
}
if (!$isHtml && $xinclude) {
$document->xinclude();
}
foreach (\libxml_get_errors() as $error) {
$message .= "\n" . $error->message;
}
\libxml_use_internal_errors($internal);
\error_reporting($reporting);
if (isset($cwd)) {
@\chdir($cwd);
}
if ($loaded === false || ($strict && $message !== '')) {
if ($filename !== '') {
throw new Exception(
\sprintf(
'Could not load "%s".%s',
$filename,
$message !== '' ? "\n" . $message : ''
)
);
}
if ($message === '') {
$message = 'Could not load XML for unknown reason';
}
throw new Exception($message);
}
return $document;
}
/**
* Loads an XML (or HTML) file into a DOMDocument object.
*
* @throws Exception
*/
public static function loadFile(string $filename, bool $isHtml = false, bool $xinclude = false, bool $strict = false): DOMDocument
{
$reporting = \error_reporting(0);
$contents = \file_get_contents($filename);
\error_reporting($reporting);
if ($contents === false) {
throw new Exception(
\sprintf(
'Could not read "%s".',
$filename
)
);
}
return self::load($contents, $isHtml, $filename, $xinclude, $strict);
}
public static function removeCharacterDataNodes(DOMNode $node): void
{
if ($node->hasChildNodes()) {
for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
$node->removeChild($child);
}
}
}
}
/**
* Escapes a string for the use in XML documents
*
* Any Unicode character is allowed, excluding the surrogate blocks, FFFE,
* and FFFF (not even as character reference).
*
* @see https://www.w3.org/TR/xml/#charsets
*/
public static function prepareString(string $string): string
{
return \preg_replace(
'/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/',
'',
\htmlspecialchars(
self::convertToUtf8($string),
\ENT_QUOTES
)
);
}
/**
* "Convert" a DOMElement object into a PHP variable.
*/
public static function xmlToVariable(DOMElement $element)
{
$variable = null;
switch ($element->tagName) {
case 'array':
$variable = [];
foreach ($element->childNodes as $entry) {
if (!$entry instanceof DOMElement || $entry->tagName !== 'element') {
continue;
}
$item = $entry->childNodes->item(0);
if ($item instanceof DOMText) {
$item = $entry->childNodes->item(1);
}
$value = self::xmlToVariable($item);
if ($entry->hasAttribute('key')) {
$variable[(string) $entry->getAttribute('key')] = $value;
} else {
$variable[] = $value;
}
}
break;
case 'object':
$className = $element->getAttribute('class');
if ($element->hasChildNodes()) {
$arguments = $element->childNodes->item(0)->childNodes;
$constructorArgs = [];
foreach ($arguments as $argument) {
if ($argument instanceof DOMElement) {
$constructorArgs[] = self::xmlToVariable($argument);
}
}
$class = new ReflectionClass($className);
$variable = $class->newInstanceArgs($constructorArgs);
} else {
$variable = new $className;
}
break;
case 'boolean':
$variable = $element->textContent === 'true';
break;
case 'integer':
case 'double':
case 'string':
$variable = $element->textContent;
\settype($variable, $element->tagName);
break;
}
return $variable;
}
private static function convertToUtf8(string $string): string
{
if (!self::isUtf8($string)) {
$string = \mb_convert_encoding($string, 'UTF-8');
}
return $string;
}
private static function isUtf8(string $string): bool
{
$length = \strlen($string);
for ($i = 0; $i < $length; $i++) {
if (\ord($string[$i]) < 0x80) {
$n = 0;
} elseif ((\ord($string[$i]) & 0xE0) === 0xC0) {
$n = 1;
} elseif ((\ord($string[$i]) & 0xF0) === 0xE0) {
$n = 2;
} elseif ((\ord($string[$i]) & 0xF0) === 0xF0) {
$n = 3;
} else {
return false;
}
for ($j = 0; $j < $n; $j++) {
if ((++$i === $length) || ((\ord($string[$i]) & 0xC0) !== 0x80)) {
return false;
}
}
}
return true;
}
}
src/Util/ConfigurationGenerator.php 0000666 00000003301 13436751637 0013457 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
final class ConfigurationGenerator
{
/**
* @var string
*/
private const TEMPLATE = <<
{tests_directory}
{src_directory}
EOT;
public function generateDefaultConfiguration(string $phpunitVersion, string $bootstrapScript, string $testsDirectory, string $srcDirectory): string
{
return \str_replace(
[
'{phpunit_version}',
'{bootstrap_script}',
'{tests_directory}',
'{src_directory}',
],
[
$phpunitVersion,
$bootstrapScript,
$testsDirectory,
$srcDirectory,
],
self::TEMPLATE
);
}
}
src/Util/TestResultCache.php 0000666 00000011352 13436751637 0012050 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner;
class TestResultCache implements \Serializable, TestResultCacheInterface
{
/**
* @var string
*/
public const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
/**
* Provide extra protection against incomplete or corrupt caches
*
* @var array
*/
private const ALLOWED_CACHE_TEST_STATUSES = [
BaseTestRunner::STATUS_SKIPPED,
BaseTestRunner::STATUS_INCOMPLETE,
BaseTestRunner::STATUS_FAILURE,
BaseTestRunner::STATUS_ERROR,
BaseTestRunner::STATUS_RISKY,
BaseTestRunner::STATUS_WARNING,
];
/**
* Path and filename for result cache file
*
* @var string
*/
private $cacheFilename;
/**
* The list of defective tests
*
*
* // Mark a test skipped
* $this->defects[$testName] = BaseTestRunner::TEST_SKIPPED;
*
*
* @var array array
*/
private $defects = [];
/**
* The list of execution duration of suites and tests (in seconds)
*
*
* // Record running time for test
* $this->times[$testName] = 1.234;
*
*
* @var array
*/
private $times = [];
public function __construct($filename = null)
{
$this->cacheFilename = $filename ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME;
}
public function persist(): void
{
$this->saveToFile();
}
public function saveToFile(): void
{
if (\defined('PHPUNIT_TESTSUITE_RESULTCACHE')) {
return;
}
if (!$this->createDirectory(\dirname($this->cacheFilename))) {
throw new Exception(
\sprintf(
'Cannot create directory "%s" for result cache file',
$this->cacheFilename
)
);
}
\file_put_contents(
$this->cacheFilename,
\serialize($this)
);
}
public function setState(string $testName, int $state): void
{
if ($state !== BaseTestRunner::STATUS_PASSED) {
$this->defects[$testName] = $state;
}
}
public function getState($testName): int
{
return $this->defects[$testName] ?? BaseTestRunner::STATUS_UNKNOWN;
}
public function setTime(string $testName, float $time): void
{
$this->times[$testName] = $time;
}
public function getTime($testName): float
{
return $this->times[$testName] ?? 0;
}
public function load(): void
{
$this->clear();
if (\is_file($this->cacheFilename) === false) {
return;
}
$cacheData = @\file_get_contents($this->cacheFilename);
// @codeCoverageIgnoreStart
if ($cacheData === false) {
return;
}
// @codeCoverageIgnoreEnd
$cache = @\unserialize($cacheData, ['allowed_classes' => [self::class]]);
if ($cache === false) {
return;
}
if ($cache instanceof self) {
/* @var \PHPUnit\Runner\TestResultCache */
$cache->copyStateToCache($this);
}
}
public function copyStateToCache(self $targetCache): void
{
foreach ($this->defects as $name => $state) {
$targetCache->setState($name, $state);
}
foreach ($this->times as $name => $time) {
$targetCache->setTime($name, $time);
}
}
public function clear(): void
{
$this->defects = [];
$this->times = [];
}
public function serialize(): string
{
return \serialize([
'defects' => $this->defects,
'times' => $this->times,
]);
}
public function unserialize($serialized): void
{
$data = \unserialize($serialized);
if (isset($data['times'])) {
foreach ($data['times'] as $testName => $testTime) {
$this->times[$testName] = (float) $testTime;
}
}
if (isset($data['defects'])) {
foreach ($data['defects'] as $testName => $testResult) {
if (\in_array($testResult, self::ALLOWED_CACHE_TEST_STATUSES, true)) {
$this->defects[$testName] = $testResult;
}
}
}
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}
src/Util/Json.php 0000666 00000004661 13436751637 0007724 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PHPUnit\Framework\Exception;
final class Json
{
/**
* Prettify json string
*
* @throws \PHPUnit\Framework\Exception
*/
public static function prettify(string $json): string
{
$decodedJson = \json_decode($json, true);
if (\json_last_error()) {
throw new Exception(
'Cannot prettify invalid json'
);
}
return \json_encode($decodedJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES);
}
/*
* To allow comparison of JSON strings, first process them into a consistent
* format so that they can be compared as strings.
* @return array ($error, $canonicalized_json) The $error parameter is used
* to indicate an error decoding the json. This is used to avoid ambiguity
* with JSON strings consisting entirely of 'null' or 'false'.
*/
public static function canonicalize(string $json): array
{
$decodedJson = \json_decode($json);
if (\json_last_error()) {
return [true, null];
}
self::recursiveSort($decodedJson);
$reencodedJson = \json_encode($decodedJson);
return [false, $reencodedJson];
}
/*
* JSON object keys are unordered while PHP array keys are ordered.
* Sort all array keys to ensure both the expected and actual values have
* their keys in the same order.
*/
private static function recursiveSort(&$json): void
{
if (\is_array($json) === false) {
// If the object is not empty, change it to an associative array
// so we can sort the keys (and we will still re-encode it
// correctly, since PHP encodes associative arrays as JSON objects.)
// But EMPTY objects MUST remain empty objects. (Otherwise we will
// re-encode it as a JSON array rather than a JSON object.)
// See #2919.
if (\is_object($json) && \count((array) $json) > 0) {
$json = (array) $json;
} else {
return;
}
}
\ksort($json);
foreach ($json as $key => &$value) {
self::recursiveSort($value);
}
}
}
src/Util/RegularExpression.php 0000666 00000001305 13436751637 0012464 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
final class RegularExpression
{
/**
* @throws \Exception
*
* @return false|int
*/
public static function safeMatch(string $pattern, string $subject, ?array $matches = null, int $flags = 0, int $offset = 0)
{
$handler_terminator = ErrorHandler::handleErrorOnce();
$match = \preg_match($pattern, $subject, $matches, $flags, $offset);
$handler_terminator();
return $match;
}
}
src/Util/TestDox/TextResultPrinter.php 0000666 00000002064 13436751637 0014067 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
/**
* Prints TestDox documentation in text format to files.
* For the CLI testdox printer please refer to \PHPUnit\TextUI\TextDoxPrinter.
*/
class TextResultPrinter extends ResultPrinter
{
/**
* Handler for 'start class' event.
*/
protected function startClass(string $name): void
{
$this->write($this->currentTestClassPrettified . "\n");
}
/**
* Handler for 'on test' event.
*/
protected function onTest($name, bool $success = true): void
{
if ($success) {
$this->write(' [x] ');
} else {
$this->write(' [ ] ');
}
$this->write($name . "\n");
}
/**
* Handler for 'end class' event.
*/
protected function endClass(string $name): void
{
$this->write("\n");
}
}
src/Util/TestDox/HtmlResultPrinter.php 0000666 00000005172 13436751637 0014052 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
/**
* Prints TestDox documentation in HTML format.
*/
final class HtmlResultPrinter extends ResultPrinter
{
/**
* @var string
*/
private const PAGE_HEADER = <<
Test Documentation
EOT;
/**
* @var string
*/
private const CLASS_HEADER = <<%s
EOT;
/**
* @var string
*/
private const CLASS_FOOTER = <<
EOT;
/**
* @var string
*/
private const PAGE_FOOTER = <<
EOT;
/**
* Handler for 'start run' event.
*/
protected function startRun(): void
{
$this->write(self::PAGE_HEADER);
}
/**
* Handler for 'start class' event.
*/
protected function startClass(string $name): void
{
$this->write(
\sprintf(
self::CLASS_HEADER,
$name,
$this->currentTestClassPrettified
)
);
}
/**
* Handler for 'on test' event.
*/
protected function onTest($name, bool $success = true): void
{
$this->write(
\sprintf(
" - %s %s
\n",
$success ? '#555753' : '#ef2929',
$success ? '✓' : 'âŒ',
$name
)
);
}
/**
* Handler for 'end class' event.
*/
protected function endClass(string $name): void
{
$this->write(self::CLASS_FOOTER);
}
/**
* Handler for 'end run' event.
*/
protected function endRun(): void
{
$this->write(self::PAGE_FOOTER);
}
}
src/Util/TestDox/CliTestDoxPrinter.php 0000666 00000013100 13436751637 0013757 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\TextUI\ResultPrinter;
use PHPUnit\Util\TestDox\TestResult as TestDoxTestResult;
use SebastianBergmann\Timer\Timer;
/**
* This printer is for CLI output only. For the classes that output to file, html and xml,
* please refer to the PHPUnit\Util\TestDox namespace
*/
class CliTestDoxPrinter extends ResultPrinter
{
/**
* @var TestDoxTestResult
*/
private $currentTestResult;
/**
* @var TestDoxTestResult
*/
private $previousTestResult;
/**
* @var TestDoxTestResult[]
*/
private $nonSuccessfulTestResults = [];
/**
* @var NamePrettifier
*/
private $prettifier;
public function __construct($out = null, bool $verbose = false, $colors = self::COLOR_DEFAULT, bool $debug = false, $numberOfColumns = 80, bool $reverse = false)
{
parent::__construct($out, $verbose, $colors, $debug, $numberOfColumns, $reverse);
$this->prettifier = new NamePrettifier;
}
public function startTest(Test $test): void
{
if (!$test instanceof TestCase && !$test instanceof PhptTestCase && !$test instanceof TestSuite) {
return;
}
$class = \get_class($test);
if ($test instanceof TestCase) {
$className = $this->prettifier->prettifyTestClass($class);
$testMethod = $this->prettifier->prettifyTestCase($test);
} elseif ($test instanceof TestSuite) {
$className = $test->getName();
$testMethod = \sprintf(
'Error bootstapping suite (most likely in %s::setUpBeforeClass)',
$test->getName()
);
} elseif ($test instanceof PhptTestCase) {
$className = $class;
$testMethod = $test->getName();
}
$this->currentTestResult = new TestDoxTestResult(
function (string $color, string $buffer) {
return $this->formatWithColor($color, $buffer);
},
$className,
$testMethod
);
parent::startTest($test);
}
public function endTest(Test $test, float $time): void
{
if (!$test instanceof TestCase && !$test instanceof PhptTestCase && !$test instanceof TestSuite) {
return;
}
parent::endTest($test, $time);
$this->currentTestResult->setRuntime($time);
$this->write($this->currentTestResult->toString($this->previousTestResult, $this->verbose));
$this->previousTestResult = $this->currentTestResult;
if (!$this->currentTestResult->isTestSuccessful()) {
$this->nonSuccessfulTestResults[] = $this->currentTestResult;
}
}
public function addError(Test $test, \Throwable $t, float $time): void
{
$this->currentTestResult->fail(
$this->formatWithColor('fg-yellow', '✘'),
(string) $t
);
}
public function addWarning(Test $test, Warning $e, float $time): void
{
$this->currentTestResult->fail(
$this->formatWithColor('fg-yellow', '✘'),
(string) $e
);
}
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->currentTestResult->fail(
$this->formatWithColor('fg-red', '✘'),
(string) $e
);
}
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
{
$this->currentTestResult->fail(
$this->formatWithColor('fg-yellow', '∅'),
(string) $t,
true
);
}
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
{
$this->currentTestResult->fail(
$this->formatWithColor('fg-yellow', '☢'),
(string) $t,
true
);
}
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
{
$this->currentTestResult->fail(
$this->formatWithColor('fg-yellow', '→'),
(string) $t,
true
);
}
public function writeProgress(string $progress): void
{
}
public function flush(): void
{
}
public function printResult(TestResult $result): void
{
$this->printHeader();
$this->printNonSuccessfulTestsSummary($result->count());
$this->printFooter($result);
}
protected function printHeader(): void
{
$this->write("\n" . Timer::resourceUsage() . "\n\n");
}
private function printNonSuccessfulTestsSummary(int $numberOfExecutedTests): void
{
$numberOfNonSuccessfulTests = \count($this->nonSuccessfulTestResults);
if ($numberOfNonSuccessfulTests === 0) {
return;
}
if (($numberOfNonSuccessfulTests / $numberOfExecutedTests) >= 0.7) {
return;
}
$this->write("Summary of non-successful tests:\n\n");
$previousTestResult = null;
foreach ($this->nonSuccessfulTestResults as $testResult) {
$this->write($testResult->toString($previousTestResult, $this->verbose));
$previousTestResult = $testResult;
}
}
}
src/Util/TestDox/TestResult.php 0000666 00000007610 13436751637 0012520 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
final class TestResult
{
/**
* @var callable
*/
private $colorize;
/**
* @var string
*/
private $testClass;
/**
* @var string
*/
private $testMethod;
/**
* @var bool
*/
private $testSuccesful;
/**
* @var string
*/
private $symbol;
/**
* @var string
*/
private $additionalInformation;
/**
* @var bool
*/
private $additionalInformationVerbose;
/**
* @var float
*/
private $runtime;
public function __construct(callable $colorize, string $testClass, string $testMethod)
{
$this->colorize = $colorize;
$this->testClass = $testClass;
$this->testMethod = $testMethod;
$this->testSuccesful = true;
$this->symbol = ($this->colorize)('fg-green', '✔');
$this->additionalInformation = '';
}
public function isTestSuccessful(): bool
{
return $this->testSuccesful;
}
public function fail(string $symbol, string $additionalInformation, bool $additionalInformationVerbose = false): void
{
$this->testSuccesful = false;
$this->symbol = $symbol;
$this->additionalInformation = $additionalInformation;
$this->additionalInformationVerbose = $additionalInformationVerbose;
}
public function setRuntime(float $runtime): void
{
$this->runtime = $runtime;
}
public function toString(?self $previousTestResult, $verbose = false): string
{
return \sprintf(
"%s%s %s %s%s\n%s",
$previousTestResult && $previousTestResult->additionalInformationPrintable($verbose) ? "\n" : '',
$this->getClassNameHeader($previousTestResult ? $previousTestResult->testClass : null),
$this->symbol,
$this->testMethod,
$verbose ? ' ' . $this->getFormattedRuntime() : '',
$this->getFormattedAdditionalInformation($verbose)
);
}
private function getClassNameHeader(?string $previousTestClass): string
{
$className = '';
if ($this->testClass !== $previousTestClass) {
if (null !== $previousTestClass) {
$className = "\n";
}
$className .= \sprintf("%s\n", $this->testClass);
}
return $className;
}
private function getFormattedRuntime(): string
{
if ($this->runtime > 5) {
return ($this->colorize)('fg-red', \sprintf('[%.2f ms]', $this->runtime * 1000));
}
if ($this->runtime > 1) {
return ($this->colorize)('fg-yellow', \sprintf('[%.2f ms]', $this->runtime * 1000));
}
return \sprintf('[%.2f ms]', $this->runtime * 1000);
}
private function getFormattedAdditionalInformation($verbose): string
{
if (!$this->additionalInformationPrintable($verbose)) {
return '';
}
return \sprintf(
" │\n%s\n",
\implode(
"\n",
\array_map(
function (string $text) {
return \sprintf(' │ %s', $text);
},
\explode("\n", $this->additionalInformation)
)
)
);
}
private function additionalInformationPrintable(bool $verbose): bool
{
if ($this->additionalInformation === '') {
return false;
}
if ($this->additionalInformationVerbose && !$verbose) {
return false;
}
return true;
}
}
src/Util/TestDox/NamePrettifier.php 0000666 00000012665 13436751637 0013326 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\Exporter\Exporter;
/**
* Prettifies class and method names for use in TestDox documentation.
*/
final class NamePrettifier
{
/**
* @var array
*/
private $strings = [];
/**
* Prettifies the name of a test class.
*/
public function prettifyTestClass(string $className): string
{
try {
$annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations($className);
if (isset($annotations['class']['testdox'][0])) {
return $annotations['class']['testdox'][0];
}
} catch (\ReflectionException $e) {
}
$result = $className;
if (\substr($className, -1 * \strlen('Test')) === 'Test') {
$result = \substr($result, 0, \strripos($result, 'Test'));
}
if (\strpos($className, 'Tests') === 0) {
$result = \substr($result, \strlen('Tests'));
} elseif (\strpos($className, 'Test') === 0) {
$result = \substr($result, \strlen('Test'));
}
if ($result[0] === '\\') {
$result = \substr($result, 1);
}
return $result;
}
/**
* @throws \ReflectionException
*/
public function prettifyTestCase(TestCase $test): string
{
$annotations = $test->getAnnotations();
$annotationWithPlaceholders = false;
$callback = static function (string $variable): string {
return \sprintf('/%s(?=\b)/', \preg_quote($variable, '/'));
};
if (isset($annotations['method']['testdox'][0])) {
$result = $annotations['method']['testdox'][0];
if (\strpos($result, '$') !== false) {
$annotation = $annotations['method']['testdox'][0];
$providedData = $this->mapTestMethodParameterNamesToProvidedDataValues($test);
$variables = \array_map($callback, \array_keys($providedData));
$result = \trim(\preg_replace($variables, $providedData, $annotation));
$annotationWithPlaceholders = true;
}
} else {
$result = $this->prettifyTestMethod($test->getName(false));
}
if ($test->usesDataProvider() && !$annotationWithPlaceholders) {
$result .= $test->getDataSetAsString(false);
}
return $result;
}
/**
* Prettifies the name of a test method.
*/
public function prettifyTestMethod(string $name): string
{
$buffer = '';
if (!\is_string($name) || $name === '') {
return $buffer;
}
$string = \preg_replace('#\d+$#', '', $name, -1, $count);
if (\in_array($string, $this->strings)) {
$name = $string;
} elseif ($count === 0) {
$this->strings[] = $string;
}
if (\strpos($name, 'test_') === 0) {
$name = \substr($name, 5);
} elseif (\strpos($name, 'test') === 0) {
$name = \substr($name, 4);
}
if ($name === '') {
return $buffer;
}
$name[0] = \strtoupper($name[0]);
if (\strpos($name, '_') !== false) {
return \trim(\str_replace('_', ' ', $name));
}
$max = \strlen($name);
$wasNumeric = false;
for ($i = 0; $i < $max; $i++) {
if ($i > 0 && \ord($name[$i]) >= 65 && \ord($name[$i]) <= 90) {
$buffer .= ' ' . \strtolower($name[$i]);
} else {
$isNumeric = \is_numeric($name[$i]);
if (!$wasNumeric && $isNumeric) {
$buffer .= ' ';
$wasNumeric = true;
}
if ($wasNumeric && !$isNumeric) {
$wasNumeric = false;
}
$buffer .= $name[$i];
}
}
return $buffer;
}
/**
* @throws \ReflectionException
*/
private function mapTestMethodParameterNamesToProvidedDataValues(TestCase $test): array
{
$reflector = new \ReflectionMethod(\get_class($test), $test->getName(false));
$providedData = [];
$providedDataValues = \array_values($test->getProvidedData());
$i = 0;
foreach ($reflector->getParameters() as $parameter) {
if (!\array_key_exists($i, $providedDataValues) && $parameter->isDefaultValueAvailable()) {
$providedDataValues[$i] = $parameter->getDefaultValue();
}
$value = $providedDataValues[$i++] ?? null;
if (\is_object($value)) {
$reflector = new \ReflectionObject($value);
if ($reflector->hasMethod('__toString')) {
$value = (string) $value;
}
}
if (!\is_scalar($value)) {
$value = \gettype($value);
}
if (\is_bool($value) || \is_int($value) || \is_float($value)) {
$exporter = new Exporter;
$value = $exporter->export($value);
}
$providedData['$' . $parameter->getName()] = $value;
}
return $providedData;
}
}
src/Util/TestDox/XmlResultPrinter.php 0000666 00000012335 13436751637 0013705 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
use DOMDocument;
use DOMElement;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\Util\Printer;
use ReflectionClass;
class XmlResultPrinter extends Printer implements TestListener
{
/**
* @var DOMDocument
*/
private $document;
/**
* @var DOMElement
*/
private $root;
/**
* @var NamePrettifier
*/
private $prettifier;
/**
* @var null|\Throwable
*/
private $exception;
/**
* @param resource|string $out
*
* @throws Exception
*/
public function __construct($out = null)
{
$this->document = new DOMDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$this->root = $this->document->createElement('tests');
$this->document->appendChild($this->root);
$this->prettifier = new NamePrettifier;
parent::__construct($out);
}
/**
* Flush buffer and close output.
*/
public function flush(): void
{
$this->write($this->document->saveXML());
parent::flush();
}
/**
* An error occurred.
*/
public function addError(Test $test, \Throwable $t, float $time): void
{
$this->exception = $t;
}
/**
* A warning occurred.
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
}
/**
* A failure occurred.
*/
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->exception = $e;
}
/**
* Incomplete test.
*/
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
{
}
/**
* Risky test.
*/
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
{
}
/**
* Skipped test.
*/
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
{
}
/**
* A test suite started.
*/
public function startTestSuite(TestSuite $suite): void
{
}
/**
* A test suite ended.
*/
public function endTestSuite(TestSuite $suite): void
{
}
/**
* A test started.
*/
public function startTest(Test $test): void
{
$this->exception = null;
}
/**
* A test ended.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function endTest(Test $test, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
/* @var TestCase $test */
$groups = \array_filter(
$test->getGroups(),
function ($group) {
return !($group === 'small' || $group === 'medium' || $group === 'large');
}
);
$node = $this->document->createElement('test');
$node->setAttribute('className', \get_class($test));
$node->setAttribute('methodName', $test->getName());
$node->setAttribute('prettifiedClassName', $this->prettifier->prettifyTestClass(\get_class($test)));
$node->setAttribute('prettifiedMethodName', $this->prettifier->prettifyTestCase($test));
$node->setAttribute('status', $test->getStatus());
$node->setAttribute('time', $time);
$node->setAttribute('size', $test->getSize());
$node->setAttribute('groups', \implode(',', $groups));
$inlineAnnotations = \PHPUnit\Util\Test::getInlineAnnotations(\get_class($test), $test->getName());
if (isset($inlineAnnotations['given'], $inlineAnnotations['when'], $inlineAnnotations['then'])) {
$node->setAttribute('given', $inlineAnnotations['given']['value']);
$node->setAttribute('givenStartLine', $inlineAnnotations['given']['line']);
$node->setAttribute('when', $inlineAnnotations['when']['value']);
$node->setAttribute('whenStartLine', $inlineAnnotations['when']['line']);
$node->setAttribute('then', $inlineAnnotations['then']['value']);
$node->setAttribute('thenStartLine', $inlineAnnotations['then']['line']);
}
if ($this->exception !== null) {
if ($this->exception instanceof Exception) {
$steps = $this->exception->getSerializableTrace();
} else {
$steps = $this->exception->getTrace();
}
$class = new ReflectionClass($test);
$file = $class->getFileName();
foreach ($steps as $step) {
if (isset($step['file']) && $step['file'] === $file) {
$node->setAttribute('exceptionLine', $step['line']);
break;
}
}
$node->setAttribute('exceptionMessage', $this->exception->getMessage());
}
$this->root->appendChild($node);
}
}
src/Util/TestDox/ResultPrinter.php 0000666 00000015543 13436751637 0013230 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\Framework\WarningTestCase;
use PHPUnit\Runner\BaseTestRunner;
use PHPUnit\Util\Printer;
/**
* Base class for printers of TestDox documentation.
*/
abstract class ResultPrinter extends Printer implements TestListener
{
/**
* @var NamePrettifier
*/
protected $prettifier;
/**
* @var string
*/
protected $testClass = '';
/**
* @var int
*/
protected $testStatus;
/**
* @var array
*/
protected $tests = [];
/**
* @var int
*/
protected $successful = 0;
/**
* @var int
*/
protected $warned = 0;
/**
* @var int
*/
protected $failed = 0;
/**
* @var int
*/
protected $risky = 0;
/**
* @var int
*/
protected $skipped = 0;
/**
* @var int
*/
protected $incomplete = 0;
/**
* @var null|string
*/
protected $currentTestClassPrettified;
/**
* @var null|string
*/
protected $currentTestMethodPrettified;
/**
* @var array
*/
private $groups;
/**
* @var array
*/
private $excludeGroups;
/**
* @param resource $out
*
* @throws \PHPUnit\Framework\Exception
*/
public function __construct($out = null, array $groups = [], array $excludeGroups = [])
{
parent::__construct($out);
$this->groups = $groups;
$this->excludeGroups = $excludeGroups;
$this->prettifier = new NamePrettifier;
$this->startRun();
}
/**
* Flush buffer and close output.
*/
public function flush(): void
{
$this->doEndClass();
$this->endRun();
parent::flush();
}
/**
* An error occurred.
*/
public function addError(Test $test, \Throwable $t, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->testStatus = BaseTestRunner::STATUS_ERROR;
$this->failed++;
}
/**
* A warning occurred.
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->testStatus = BaseTestRunner::STATUS_WARNING;
$this->warned++;
}
/**
* A failure occurred.
*/
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->testStatus = BaseTestRunner::STATUS_FAILURE;
$this->failed++;
}
/**
* Incomplete test.
*/
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->testStatus = BaseTestRunner::STATUS_INCOMPLETE;
$this->incomplete++;
}
/**
* Risky test.
*/
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->testStatus = BaseTestRunner::STATUS_RISKY;
$this->risky++;
}
/**
* Skipped test.
*/
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->testStatus = BaseTestRunner::STATUS_SKIPPED;
$this->skipped++;
}
/**
* A testsuite started.
*/
public function startTestSuite(TestSuite $suite): void
{
}
/**
* A testsuite ended.
*/
public function endTestSuite(TestSuite $suite): void
{
}
/**
* A test started.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function startTest(Test $test): void
{
if (!$this->isOfInterest($test)) {
return;
}
$class = \get_class($test);
if ($this->testClass !== $class) {
if ($this->testClass !== '') {
$this->doEndClass();
}
$this->currentTestClassPrettified = $this->prettifier->prettifyTestClass($class);
$this->testClass = $class;
$this->tests = [];
$this->startClass($class);
}
if ($test instanceof TestCase) {
$this->currentTestMethodPrettified = $this->prettifier->prettifyTestCase($test);
}
$this->testStatus = BaseTestRunner::STATUS_PASSED;
}
/**
* A test ended.
*/
public function endTest(Test $test, float $time): void
{
if (!$this->isOfInterest($test)) {
return;
}
$this->tests[] = [$this->currentTestMethodPrettified, $this->testStatus];
$this->currentTestClassPrettified = null;
$this->currentTestMethodPrettified = null;
}
protected function doEndClass(): void
{
foreach ($this->tests as $test) {
$this->onTest($test[0], $test[1] === BaseTestRunner::STATUS_PASSED);
}
$this->endClass($this->testClass);
}
/**
* Handler for 'start run' event.
*/
protected function startRun(): void
{
}
/**
* Handler for 'start class' event.
*/
protected function startClass(string $name): void
{
}
/**
* Handler for 'on test' event.
*/
protected function onTest($name, bool $success = true): void
{
}
/**
* Handler for 'end class' event.
*/
protected function endClass(string $name): void
{
}
/**
* Handler for 'end run' event.
*/
protected function endRun(): void
{
}
private function isOfInterest(Test $test): bool
{
if (!$test instanceof TestCase) {
return false;
}
if ($test instanceof WarningTestCase) {
return false;
}
if (!empty($this->groups)) {
foreach ($test->getGroups() as $group) {
if (\in_array($group, $this->groups)) {
return true;
}
}
return false;
}
if (!empty($this->excludeGroups)) {
foreach ($test->getGroups() as $group) {
if (\in_array($group, $this->excludeGroups)) {
return false;
}
}
return true;
}
return true;
}
}
src/Util/Test.php 0000666 00000111453 13436751637 0007730 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PharIo\Version\VersionConstraintParser;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\CodeCoverageException;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\InvalidCoversTargetException;
use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\SkippedTestError;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Warning;
use PHPUnit\Runner\Version;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use SebastianBergmann\Environment\OperatingSystem;
use Traversable;
final class Test
{
/**
* @var int
*/
public const UNKNOWN = -1;
/**
* @var int
*/
public const SMALL = 0;
/**
* @var int
*/
public const MEDIUM = 1;
/**
* @var int
*/
public const LARGE = 2;
/**
* @var string
*
* @todo This constant should be private (it's public because of TestTest::testGetProvidedDataRegEx)
*/
public const REGEX_DATA_PROVIDER = '/@dataProvider\s+([a-zA-Z0-9._:-\\\\x7f-\xff]+)/';
/**
* @var string
*/
private const REGEX_TEST_WITH = '/@testWith\s+/';
/**
* @var string
*/
private const REGEX_EXPECTED_EXCEPTION = '(@expectedException\s+([:.\w\\\\x7f-\xff]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m';
/**
* @var string
*/
private const REGEX_REQUIRES_VERSION = '/@requires\s+(?PPHP(?:Unit)?)\s+(?P[<>=!]{0,2})\s*(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m';
/**
* @var string
*/
private const REGEX_REQUIRES_VERSION_CONSTRAINT = '/@requires\s+(?PPHP(?:Unit)?)\s+(?P[\d\t -.|~^]+)[ \t]*\r?$/m';
/**
* @var string
*/
private const REGEX_REQUIRES_OS = '/@requires\s+(?POS(?:FAMILY)?)\s+(?P.+?)[ \t]*\r?$/m';
/**
* @var string
*/
private const REGEX_REQUIRES_SETTING = '/@requires\s+(?Psetting)\s+(?P([^ ]+?))\s*(?P[\w\.-]+[\w\.]?)?[ \t]*\r?$/m';
/**
* @var string
*/
private const REGEX_REQUIRES = '/@requires\s+(?Pfunction|extension)\s+(?P([^ ]+?))\s*(?P[<>=!]{0,2})\s*(?P[\d\.-]+[\d\.]?)?[ \t]*\r?$/m';
/**
* @var array
*/
private static $annotationCache = [];
/**
* @var array
*/
private static $hookMethods = [];
public static function describe(\PHPUnit\Framework\Test $test): array
{
if ($test instanceof TestCase) {
return [\get_class($test), $test->getName()];
}
if ($test instanceof SelfDescribing) {
return ['', $test->toString()];
}
return ['', \get_class($test)];
}
public static function describeAsString(\PHPUnit\Framework\Test $test): string
{
if ($test instanceof SelfDescribing) {
return $test->toString();
}
return \get_class($test);
}
/**
* @throws CodeCoverageException
*
* @return array|bool
*/
public static function getLinesToBeCovered(string $className, string $methodName)
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
if (self::shouldCoversAnnotationBeUsed($annotations) === false) {
return false;
}
return self::getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
}
/**
* Returns lines of code specified with the @uses annotation.
*
* @throws CodeCoverageException
*/
public static function getLinesToBeUsed(string $className, string $methodName): array
{
return self::getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
}
/**
* Returns the requirements for a test.
*
* @throws Warning
*/
public static function getRequirements(string $className, string $methodName): array
{
$reflector = new ReflectionClass($className);
$docComment = $reflector->getDocComment();
$reflector = new ReflectionMethod($className, $methodName);
$docComment .= "\n" . $reflector->getDocComment();
$requires = [];
if ($count = \preg_match_all(self::REGEX_REQUIRES_OS, $docComment, $matches)) {
foreach (\range(0, $count - 1) as $i) {
$requires[$matches['name'][$i]] = $matches['value'][$i];
}
}
if ($count = \preg_match_all(self::REGEX_REQUIRES_VERSION, $docComment, $matches)) {
foreach (\range(0, $count - 1) as $i) {
$requires[$matches['name'][$i]] = [
'version' => $matches['version'][$i],
'operator' => $matches['operator'][$i],
];
}
}
if ($count = \preg_match_all(self::REGEX_REQUIRES_VERSION_CONSTRAINT, $docComment, $matches)) {
foreach (\range(0, $count - 1) as $i) {
if (!empty($requires[$matches['name'][$i]])) {
continue;
}
try {
$versionConstraintParser = new VersionConstraintParser;
$requires[$matches['name'][$i] . '_constraint'] = [
'constraint' => $versionConstraintParser->parse(\trim($matches['constraint'][$i])),
];
} catch (\PharIo\Version\Exception $e) {
throw new Warning($e->getMessage(), $e->getCode(), $e);
}
}
}
if ($count = \preg_match_all(self::REGEX_REQUIRES_SETTING, $docComment, $matches)) {
$requires['setting'] = [];
foreach (\range(0, $count - 1) as $i) {
$requires['setting'][$matches['setting'][$i]] = $matches['value'][$i];
}
}
if ($count = \preg_match_all(self::REGEX_REQUIRES, $docComment, $matches)) {
foreach (\range(0, $count - 1) as $i) {
$name = $matches['name'][$i] . 's';
if (!isset($requires[$name])) {
$requires[$name] = [];
}
$requires[$name][] = $matches['value'][$i];
if ($name !== 'extensions' || empty($matches['version'][$i])) {
continue;
}
$requires['extension_versions'][$matches['value'][$i]] = [
'version' => $matches['version'][$i],
'operator' => $matches['operator'][$i],
];
}
}
return $requires;
}
/**
* Returns the missing requirements for a test.
*
* @throws Warning
*
* @return string[]
*/
public static function getMissingRequirements(string $className, string $methodName): array
{
$required = static::getRequirements($className, $methodName);
$missing = [];
if (!empty($required['PHP'])) {
$operator = empty($required['PHP']['operator']) ? '>=' : $required['PHP']['operator'];
if (!\version_compare(\PHP_VERSION, $required['PHP']['version'], $operator)) {
$missing[] = \sprintf('PHP %s %s is required.', $operator, $required['PHP']['version']);
}
} elseif (!empty($required['PHP_constraint'])) {
$version = new \PharIo\Version\Version(self::sanitizeVersionNumber(\PHP_VERSION));
if (!$required['PHP_constraint']['constraint']->complies($version)) {
$missing[] = \sprintf(
'PHP version does not match the required constraint %s.',
$required['PHP_constraint']['constraint']->asString()
);
}
}
if (!empty($required['PHPUnit'])) {
$phpunitVersion = Version::id();
$operator = empty($required['PHPUnit']['operator']) ? '>=' : $required['PHPUnit']['operator'];
if (!\version_compare($phpunitVersion, $required['PHPUnit']['version'], $operator)) {
$missing[] = \sprintf('PHPUnit %s %s is required.', $operator, $required['PHPUnit']['version']);
}
} elseif (!empty($required['PHPUnit_constraint'])) {
$phpunitVersion = new \PharIo\Version\Version(self::sanitizeVersionNumber(Version::id()));
if (!$required['PHPUnit_constraint']['constraint']->complies($phpunitVersion)) {
$missing[] = \sprintf(
'PHPUnit version does not match the required constraint %s.',
$required['PHPUnit_constraint']['constraint']->asString()
);
}
}
if (!empty($required['OSFAMILY']) && $required['OSFAMILY'] !== (new OperatingSystem)->getFamily()) {
$missing[] = \sprintf('Operating system %s is required.', $required['OSFAMILY']);
}
if (!empty($required['OS'])) {
$requiredOsPattern = \sprintf('/%s/i', \addcslashes($required['OS'], '/'));
if (!\preg_match($requiredOsPattern, \PHP_OS)) {
$missing[] = \sprintf('Operating system matching %s is required.', $requiredOsPattern);
}
}
if (!empty($required['functions'])) {
foreach ($required['functions'] as $function) {
$pieces = \explode('::', $function);
if (\count($pieces) === 2 && \method_exists($pieces[0], $pieces[1])) {
continue;
}
if (\function_exists($function)) {
continue;
}
$missing[] = \sprintf('Function %s is required.', $function);
}
}
if (!empty($required['setting'])) {
foreach ($required['setting'] as $setting => $value) {
if (\ini_get($setting) != $value) {
$missing[] = \sprintf('Setting "%s" must be "%s".', $setting, $value);
}
}
}
if (!empty($required['extensions'])) {
foreach ($required['extensions'] as $extension) {
if (isset($required['extension_versions'][$extension])) {
continue;
}
if (!\extension_loaded($extension)) {
$missing[] = \sprintf('Extension %s is required.', $extension);
}
}
}
if (!empty($required['extension_versions'])) {
foreach ($required['extension_versions'] as $extension => $required) {
$actualVersion = \phpversion($extension);
$operator = empty($required['operator']) ? '>=' : $required['operator'];
if ($actualVersion === false || !\version_compare($actualVersion, $required['version'], $operator)) {
$missing[] = \sprintf('Extension %s %s %s is required.', $extension, $operator, $required['version']);
}
}
}
return $missing;
}
/**
* Returns the expected exception for a test.
*
* @return array|false
*/
public static function getExpectedException(string $className, ?string $methodName)
{
$reflector = new ReflectionMethod($className, $methodName);
$docComment = $reflector->getDocComment();
$docComment = \substr($docComment, 3, -2);
if (\preg_match(self::REGEX_EXPECTED_EXCEPTION, $docComment, $matches)) {
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
$class = $matches[1];
$code = null;
$message = '';
$messageRegExp = '';
if (isset($matches[2])) {
$message = \trim($matches[2]);
} elseif (isset($annotations['method']['expectedExceptionMessage'])) {
$message = self::parseAnnotationContent(
$annotations['method']['expectedExceptionMessage'][0]
);
}
if (isset($annotations['method']['expectedExceptionMessageRegExp'])) {
$messageRegExp = self::parseAnnotationContent(
$annotations['method']['expectedExceptionMessageRegExp'][0]
);
}
if (isset($matches[3])) {
$code = $matches[3];
} elseif (isset($annotations['method']['expectedExceptionCode'])) {
$code = self::parseAnnotationContent(
$annotations['method']['expectedExceptionCode'][0]
);
}
if (\is_numeric($code)) {
$code = (int) $code;
} elseif (\is_string($code) && \defined($code)) {
$code = (int) \constant($code);
}
return [
'class' => $class, 'code' => $code, 'message' => $message, 'message_regex' => $messageRegExp,
];
}
return false;
}
/**
* Returns the provided data for a method.
*
* @throws Exception
*/
public static function getProvidedData(string $className, string $methodName): ?array
{
$reflector = new ReflectionMethod($className, $methodName);
$docComment = $reflector->getDocComment();
$data = self::getDataFromDataProviderAnnotation($docComment, $className, $methodName);
if ($data === null) {
$data = self::getDataFromTestWithAnnotation($docComment);
}
if (\is_array($data) && empty($data)) {
throw new SkippedTestError;
}
if ($data !== null) {
foreach ($data as $key => $value) {
if (!\is_array($value)) {
throw new Exception(
\sprintf(
'Data set %s is invalid.',
\is_int($key) ? '#' . $key : '"' . $key . '"'
)
);
}
}
}
return $data;
}
/**
* @throws Exception
*/
public static function getDataFromTestWithAnnotation(string $docComment): ?array
{
$docComment = self::cleanUpMultiLineAnnotation($docComment);
if (\preg_match(self::REGEX_TEST_WITH, $docComment, $matches, \PREG_OFFSET_CAPTURE)) {
$offset = \strlen($matches[0][0]) + $matches[0][1];
$annotationContent = \substr($docComment, $offset);
$data = [];
foreach (\explode("\n", $annotationContent) as $candidateRow) {
$candidateRow = \trim($candidateRow);
if ($candidateRow[0] !== '[') {
break;
}
$dataSet = \json_decode($candidateRow, true);
if (\json_last_error() !== \JSON_ERROR_NONE) {
throw new Exception(
'The data set for the @testWith annotation cannot be parsed: ' . \json_last_error_msg()
);
}
$data[] = $dataSet;
}
if (!$data) {
throw new Exception('The data set for the @testWith annotation cannot be parsed.');
}
return $data;
}
return null;
}
public static function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array
{
if (!isset(self::$annotationCache[$className])) {
$class = new ReflectionClass($className);
$traits = $class->getTraits();
$annotations = [];
foreach ($traits as $trait) {
$annotations = \array_merge(
$annotations,
self::parseAnnotations($trait->getDocComment())
);
}
self::$annotationCache[$className] = \array_merge(
$annotations,
self::parseAnnotations($class->getDocComment())
);
}
$cacheKey = $className . '::' . $methodName;
if ($methodName !== null && !isset(self::$annotationCache[$cacheKey])) {
try {
$method = new ReflectionMethod($className, $methodName);
$annotations = self::parseAnnotations($method->getDocComment());
} catch (ReflectionException $e) {
$annotations = [];
}
self::$annotationCache[$cacheKey] = $annotations;
}
return [
'class' => self::$annotationCache[$className],
'method' => $methodName !== null ? self::$annotationCache[$cacheKey] : [],
];
}
public static function getInlineAnnotations(string $className, string $methodName): array
{
$method = new ReflectionMethod($className, $methodName);
$code = \file($method->getFileName());
$lineNumber = $method->getStartLine();
$startLine = $method->getStartLine() - 1;
$endLine = $method->getEndLine() - 1;
$methodLines = \array_slice($code, $startLine, $endLine - $startLine + 1);
$annotations = [];
foreach ($methodLines as $line) {
if (\preg_match('#/\*\*?\s*@(?P[A-Za-z_-]+)(?:[ \t]+(?P.*?))?[ \t]*\r?\*/$#m', $line, $matches)) {
$annotations[\strtolower($matches['name'])] = [
'line' => $lineNumber,
'value' => $matches['value'],
];
}
$lineNumber++;
}
return $annotations;
}
public static function parseAnnotations(string $docBlock): array
{
$annotations = [];
// Strip away the docblock header and footer to ease parsing of one line annotations
$docBlock = \substr($docBlock, 3, -2);
if (\preg_match_all('/@(?P[A-Za-z_-]+)(?:[ \t]+(?P.*?))?[ \t]*\r?$/m', $docBlock, $matches)) {
$numMatches = \count($matches[0]);
for ($i = 0; $i < $numMatches; ++$i) {
$annotations[$matches['name'][$i]][] = (string) $matches['value'][$i];
}
}
return $annotations;
}
public static function getBackupSettings(string $className, string $methodName): array
{
return [
'backupGlobals' => self::getBooleanAnnotationSetting(
$className,
$methodName,
'backupGlobals'
),
'backupStaticAttributes' => self::getBooleanAnnotationSetting(
$className,
$methodName,
'backupStaticAttributes'
),
];
}
public static function getDependencies(string $className, string $methodName): array
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
$dependencies = [];
if (isset($annotations['class']['depends'])) {
$dependencies = $annotations['class']['depends'];
}
if (isset($annotations['method']['depends'])) {
$dependencies = \array_merge(
$dependencies,
$annotations['method']['depends']
);
}
return \array_unique($dependencies);
}
public static function getErrorHandlerSettings(string $className, ?string $methodName): ?bool
{
return self::getBooleanAnnotationSetting(
$className,
$methodName,
'errorHandler'
);
}
public static function getGroups(string $className, ?string $methodName = ''): array
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
$groups = [];
if (isset($annotations['method']['author'])) {
$groups = $annotations['method']['author'];
} elseif (isset($annotations['class']['author'])) {
$groups = $annotations['class']['author'];
}
if (isset($annotations['class']['group'])) {
$groups = \array_merge($groups, $annotations['class']['group']);
}
if (isset($annotations['method']['group'])) {
$groups = \array_merge($groups, $annotations['method']['group']);
}
if (isset($annotations['class']['ticket'])) {
$groups = \array_merge($groups, $annotations['class']['ticket']);
}
if (isset($annotations['method']['ticket'])) {
$groups = \array_merge($groups, $annotations['method']['ticket']);
}
foreach (['method', 'class'] as $element) {
foreach (['small', 'medium', 'large'] as $size) {
if (isset($annotations[$element][$size])) {
$groups[] = $size;
break 2;
}
}
}
return \array_unique($groups);
}
public static function getSize(string $className, ?string $methodName): int
{
$groups = \array_flip(self::getGroups($className, $methodName));
if (isset($groups['large'])) {
return self::LARGE;
}
if (isset($groups['medium'])) {
return self::MEDIUM;
}
if (isset($groups['small'])) {
return self::SMALL;
}
return self::UNKNOWN;
}
public static function getProcessIsolationSettings(string $className, string $methodName): bool
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
return isset($annotations['class']['runTestsInSeparateProcesses']) || isset($annotations['method']['runInSeparateProcess']);
}
public static function getClassProcessIsolationSettings(string $className, string $methodName): bool
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
return isset($annotations['class']['runClassInSeparateProcess']);
}
public static function getPreserveGlobalStateSettings(string $className, string $methodName): ?bool
{
return self::getBooleanAnnotationSetting(
$className,
$methodName,
'preserveGlobalState'
);
}
public static function getHookMethods(string $className): array
{
if (!\class_exists($className, false)) {
return self::emptyHookMethodsArray();
}
if (!isset(self::$hookMethods[$className])) {
self::$hookMethods[$className] = self::emptyHookMethodsArray();
try {
$class = new ReflectionClass($className);
foreach ($class->getMethods() as $method) {
if ($method->getDeclaringClass()->getName() === Assert::class) {
continue;
}
if ($method->getDeclaringClass()->getName() === TestCase::class) {
continue;
}
if (self::isBeforeClassMethod($method)) {
\array_unshift(
self::$hookMethods[$className]['beforeClass'],
$method->getName()
);
}
if (self::isBeforeMethod($method)) {
\array_unshift(
self::$hookMethods[$className]['before'],
$method->getName()
);
}
if (self::isAfterMethod($method)) {
self::$hookMethods[$className]['after'][] = $method->getName();
}
if (self::isAfterClassMethod($method)) {
self::$hookMethods[$className]['afterClass'][] = $method->getName();
}
}
} catch (ReflectionException $e) {
}
}
return self::$hookMethods[$className];
}
/**
* @throws CodeCoverageException
*/
private static function getLinesToBeCoveredOrUsed(string $className, string $methodName, string $mode): array
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
$classShortcut = null;
if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
if (\count($annotations['class'][$mode . 'DefaultClass']) > 1) {
throw new CodeCoverageException(
\sprintf(
'More than one @%sClass annotation in class or interface "%s".',
$mode,
$className
)
);
}
$classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
}
$list = [];
if (isset($annotations['class'][$mode])) {
$list = $annotations['class'][$mode];
}
if (isset($annotations['method'][$mode])) {
$list = \array_merge($list, $annotations['method'][$mode]);
}
$codeList = [];
foreach (\array_unique($list) as $element) {
if ($classShortcut && \strncmp($element, '::', 2) === 0) {
$element = $classShortcut . $element;
}
$element = \preg_replace('/[\s()]+$/', '', $element);
$element = \explode(' ', $element);
$element = $element[0];
if ($mode === 'covers' && \interface_exists($element)) {
throw new InvalidCoversTargetException(
\sprintf(
'Trying to @cover interface "%s".',
$element
)
);
}
$codeList = \array_merge(
$codeList,
self::resolveElementToReflectionObjects($element)
);
}
return self::resolveReflectionObjectsToLines($codeList);
}
/**
* Parse annotation content to use constant/class constant values
*
* Constants are specified using a starting '@'. For example: @ClassName::CONST_NAME
*
* If the constant is not found the string is used as is to ensure maximum BC.
*/
private static function parseAnnotationContent(string $message): string
{
if (\defined($message) && (\strpos($message, '::') !== false && \substr_count($message, '::') + 1 === 2)) {
$message = \constant($message);
}
return $message;
}
/**
* Returns the provided data for a method.
*/
private static function getDataFromDataProviderAnnotation(string $docComment, string $className, string $methodName): ?iterable
{
if (\preg_match_all(self::REGEX_DATA_PROVIDER, $docComment, $matches)) {
$result = [];
foreach ($matches[1] as $match) {
$dataProviderMethodNameNamespace = \explode('\\', $match);
$leaf = \explode('::', \array_pop($dataProviderMethodNameNamespace));
$dataProviderMethodName = \array_pop($leaf);
if (empty($dataProviderMethodNameNamespace)) {
$dataProviderMethodNameNamespace = '';
} else {
$dataProviderMethodNameNamespace = \implode('\\', $dataProviderMethodNameNamespace) . '\\';
}
if (empty($leaf)) {
$dataProviderClassName = $className;
} else {
$dataProviderClassName = $dataProviderMethodNameNamespace . \array_pop($leaf);
}
$dataProviderClass = new ReflectionClass($dataProviderClassName);
$dataProviderMethod = $dataProviderClass->getMethod(
$dataProviderMethodName
);
if ($dataProviderMethod->isStatic()) {
$object = null;
} else {
$object = $dataProviderClass->newInstance();
}
if ($dataProviderMethod->getNumberOfParameters() === 0) {
$data = $dataProviderMethod->invoke($object);
} else {
$data = $dataProviderMethod->invoke($object, $methodName);
}
if ($data instanceof Traversable) {
$origData = $data;
$data = [];
foreach ($origData as $key => $value) {
if (\is_int($key)) {
$data[] = $value;
} else {
$data[$key] = $value;
}
}
}
if (\is_array($data)) {
$result = \array_merge($result, $data);
}
}
return $result;
}
return null;
}
private static function cleanUpMultiLineAnnotation(string $docComment): string
{
//removing initial ' * ' for docComment
$docComment = \str_replace("\r\n", "\n", $docComment);
$docComment = \preg_replace('/' . '\n' . '\s*' . '\*' . '\s?' . '/', "\n", $docComment);
$docComment = \substr($docComment, 0, -1);
return \rtrim($docComment, "\n");
}
private static function emptyHookMethodsArray(): array
{
return [
'beforeClass' => ['setUpBeforeClass'],
'before' => ['setUp'],
'after' => ['tearDown'],
'afterClass' => ['tearDownAfterClass'],
];
}
private static function getBooleanAnnotationSetting(string $className, ?string $methodName, string $settingName): ?bool
{
$annotations = self::parseTestMethodAnnotations(
$className,
$methodName
);
if (isset($annotations['method'][$settingName])) {
if ($annotations['method'][$settingName][0] === 'enabled') {
return true;
}
if ($annotations['method'][$settingName][0] === 'disabled') {
return false;
}
}
if (isset($annotations['class'][$settingName])) {
if ($annotations['class'][$settingName][0] === 'enabled') {
return true;
}
if ($annotations['class'][$settingName][0] === 'disabled') {
return false;
}
}
return null;
}
/**
* @throws InvalidCoversTargetException
*/
private static function resolveElementToReflectionObjects(string $element): array
{
$codeToCoverList = [];
if (\strpos($element, '\\') !== false && \function_exists($element)) {
$codeToCoverList[] = new ReflectionFunction($element);
} elseif (\strpos($element, '::') !== false) {
[$className, $methodName] = \explode('::', $element);
if (isset($methodName[0]) && $methodName[0] === '<') {
$classes = [$className];
foreach ($classes as $className) {
if (!\class_exists($className) &&
!\interface_exists($className) &&
!\trait_exists($className)) {
throw new InvalidCoversTargetException(
\sprintf(
'Trying to @cover or @use not existing class or ' .
'interface "%s".',
$className
)
);
}
$class = new ReflectionClass($className);
$methods = $class->getMethods();
$inverse = isset($methodName[1]) && $methodName[1] === '!';
$visibility = 'isPublic';
if (\strpos($methodName, 'protected')) {
$visibility = 'isProtected';
} elseif (\strpos($methodName, 'private')) {
$visibility = 'isPrivate';
}
foreach ($methods as $method) {
if ($inverse && !$method->$visibility()) {
$codeToCoverList[] = $method;
} elseif (!$inverse && $method->$visibility()) {
$codeToCoverList[] = $method;
}
}
}
} else {
$classes = [$className];
foreach ($classes as $className) {
if ($className === '' && \function_exists($methodName)) {
$codeToCoverList[] = new ReflectionFunction(
$methodName
);
} else {
if (!((\class_exists($className) || \interface_exists($className) || \trait_exists($className)) &&
\method_exists($className, $methodName))) {
throw new InvalidCoversTargetException(
\sprintf(
'Trying to @cover or @use not existing method "%s::%s".',
$className,
$methodName
)
);
}
$codeToCoverList[] = new ReflectionMethod(
$className,
$methodName
);
}
}
}
} else {
$extended = false;
if (\strpos($element, '') !== false) {
$element = \str_replace('', '', $element);
$extended = true;
}
$classes = [$element];
if ($extended) {
$classes = \array_merge(
$classes,
\class_implements($element),
\class_parents($element)
);
}
foreach ($classes as $className) {
if (!\class_exists($className) &&
!\interface_exists($className) &&
!\trait_exists($className)) {
throw new InvalidCoversTargetException(
\sprintf(
'Trying to @cover or @use not existing class or ' .
'interface "%s".',
$className
)
);
}
$codeToCoverList[] = new ReflectionClass($className);
}
}
return $codeToCoverList;
}
private static function resolveReflectionObjectsToLines(array $reflectors): array
{
$result = [];
foreach ($reflectors as $reflector) {
if ($reflector instanceof ReflectionClass) {
foreach ($reflector->getTraits() as $trait) {
$reflectors[] = $trait;
}
}
}
foreach ($reflectors as $reflector) {
$filename = $reflector->getFileName();
if (!isset($result[$filename])) {
$result[$filename] = [];
}
$result[$filename] = \array_merge(
$result[$filename],
\range($reflector->getStartLine(), $reflector->getEndLine())
);
}
foreach ($result as $filename => $lineNumbers) {
$result[$filename] = \array_keys(\array_flip($lineNumbers));
}
return $result;
}
private static function isBeforeClassMethod(ReflectionMethod $method): bool
{
return $method->isStatic() && \strpos($method->getDocComment(), '@beforeClass') !== false;
}
private static function isBeforeMethod(ReflectionMethod $method): bool
{
return \preg_match('/@before\b/', $method->getDocComment()) > 0;
}
private static function isAfterClassMethod(ReflectionMethod $method): bool
{
return $method->isStatic() && \strpos($method->getDocComment(), '@afterClass') !== false;
}
private static function isAfterMethod(ReflectionMethod $method): bool
{
return \preg_match('/@after\b/', $method->getDocComment()) > 0;
}
/**
* Trims any extensions from version string that follows after
* the .[.] format
*/
private static function sanitizeVersionNumber(string $version)
{
return \preg_replace(
'/^(\d+\.\d+(?:.\d+)?).*$/',
'$1',
$version
);
}
private static function shouldCoversAnnotationBeUsed(array $annotations): bool
{
if (isset($annotations['method']['coversNothing'])) {
return false;
}
if (isset($annotations['method']['covers'])) {
return true;
}
if (isset($annotations['class']['coversNothing'])) {
return false;
}
return true;
}
}
src/Util/Log/JUnit.php 0000666 00000026114 13436751637 0010562 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\Log;
use DOMDocument;
use DOMElement;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExceptionWrapper;
use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestFailure;
use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\Util\Filter;
use PHPUnit\Util\Printer;
use PHPUnit\Util\Xml;
use ReflectionClass;
use ReflectionException;
/**
* A TestListener that generates a logfile of the test execution in XML markup.
*
* The XML markup used is the same as the one that is used by the JUnit Ant task.
*/
class JUnit extends Printer implements TestListener
{
/**
* @var DOMDocument
*/
protected $document;
/**
* @var DOMElement
*/
protected $root;
/**
* @var bool
*/
protected $reportUselessTests = false;
/**
* @var bool
*/
protected $writeDocument = true;
/**
* @var DOMElement[]
*/
protected $testSuites = [];
/**
* @var int[]
*/
protected $testSuiteTests = [0];
/**
* @var int[]
*/
protected $testSuiteAssertions = [0];
/**
* @var int[]
*/
protected $testSuiteErrors = [0];
/**
* @var int[]
*/
protected $testSuiteFailures = [0];
/**
* @var int[]
*/
protected $testSuiteSkipped = [0];
/**
* @var int[]
*/
protected $testSuiteTimes = [0];
/**
* @var int
*/
protected $testSuiteLevel = 0;
/**
* @var DOMElement
*/
protected $currentTestCase;
/**
* Constructor.
*
* @param null|mixed $out
*
* @throws \PHPUnit\Framework\Exception
*/
public function __construct($out = null, bool $reportUselessTests = false)
{
$this->document = new DOMDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$this->root = $this->document->createElement('testsuites');
$this->document->appendChild($this->root);
parent::__construct($out);
$this->reportUselessTests = $reportUselessTests;
}
/**
* Flush buffer and close output.
*/
public function flush(): void
{
if ($this->writeDocument === true) {
$this->write($this->getXML());
}
parent::flush();
}
/**
* An error occurred.
*
* @throws \InvalidArgumentException
*/
public function addError(Test $test, \Throwable $t, float $time): void
{
$this->doAddFault($test, $t, $time, 'error');
$this->testSuiteErrors[$this->testSuiteLevel]++;
}
/**
* A warning occurred.
*
* @throws \InvalidArgumentException
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
$this->doAddFault($test, $e, $time, 'warning');
$this->testSuiteFailures[$this->testSuiteLevel]++;
}
/**
* A failure occurred.
*
* @throws \InvalidArgumentException
*/
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->doAddFault($test, $e, $time, 'failure');
$this->testSuiteFailures[$this->testSuiteLevel]++;
}
/**
* Incomplete test.
*/
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
{
$this->doAddSkipped($test);
}
/**
* Risky test.
*/
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
{
if (!$this->reportUselessTests || $this->currentTestCase === null) {
return;
}
$error = $this->document->createElement(
'error',
Xml::prepareString(
"Risky Test\n" .
Filter::getFilteredStacktrace($t)
)
);
$error->setAttribute('type', \get_class($t));
$this->currentTestCase->appendChild($error);
$this->testSuiteErrors[$this->testSuiteLevel]++;
}
/**
* Skipped test.
*/
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
{
$this->doAddSkipped($test);
}
/**
* A testsuite started.
*/
public function startTestSuite(TestSuite $suite): void
{
$testSuite = $this->document->createElement('testsuite');
$testSuite->setAttribute('name', $suite->getName());
if (\class_exists($suite->getName(), false)) {
try {
$class = new ReflectionClass($suite->getName());
$testSuite->setAttribute('file', $class->getFileName());
} catch (ReflectionException $e) {
}
}
if ($this->testSuiteLevel > 0) {
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
} else {
$this->root->appendChild($testSuite);
}
$this->testSuiteLevel++;
$this->testSuites[$this->testSuiteLevel] = $testSuite;
$this->testSuiteTests[$this->testSuiteLevel] = 0;
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
}
/**
* A testsuite ended.
*/
public function endTestSuite(TestSuite $suite): void
{
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'tests',
$this->testSuiteTests[$this->testSuiteLevel]
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'assertions',
$this->testSuiteAssertions[$this->testSuiteLevel]
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'errors',
$this->testSuiteErrors[$this->testSuiteLevel]
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'failures',
$this->testSuiteFailures[$this->testSuiteLevel]
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'skipped',
$this->testSuiteSkipped[$this->testSuiteLevel]
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'time',
\sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel])
);
if ($this->testSuiteLevel > 1) {
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
}
$this->testSuiteLevel--;
}
/**
* A test started.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
* @throws ReflectionException
*/
public function startTest(Test $test): void
{
if (!$test instanceof TestCase) {
return;
}
$testCase = $this->document->createElement('testcase');
$testCase->setAttribute('name', $test->getName());
$class = new ReflectionClass($test);
$methodName = $test->getName(!$test->usesDataProvider());
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);
$testCase->setAttribute('class', $class->getName());
$testCase->setAttribute('classname', \str_replace('\\', '.', $class->getName()));
$testCase->setAttribute('file', $class->getFileName());
$testCase->setAttribute('line', $method->getStartLine());
}
$this->currentTestCase = $testCase;
}
/**
* A test ended.
*/
public function endTest(Test $test, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$numAssertions = $test->getNumAssertions();
$this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
$this->currentTestCase->setAttribute(
'assertions',
$numAssertions
);
$this->currentTestCase->setAttribute(
'time',
\sprintf('%F', $time)
);
$this->testSuites[$this->testSuiteLevel]->appendChild(
$this->currentTestCase
);
$this->testSuiteTests[$this->testSuiteLevel]++;
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
if ($test->hasOutput()) {
$systemOut = $this->document->createElement(
'system-out',
Xml::prepareString($test->getActualOutput())
);
$this->currentTestCase->appendChild($systemOut);
}
$this->currentTestCase = null;
}
/**
* Returns the XML as a string.
*/
public function getXML(): string
{
return $this->document->saveXML();
}
/**
* Enables or disables the writing of the document
* in flush().
*
* This is a "hack" needed for the integration of
* PHPUnit with Phing.
*/
public function setWriteDocument(/*bool*/ $flag): void
{
if (\is_bool($flag)) {
$this->writeDocument = $flag;
}
}
/**
* Method which generalizes addError() and addFailure()
*
* @throws \InvalidArgumentException
*/
private function doAddFault(Test $test, \Throwable $t, float $time, $type): void
{
if ($this->currentTestCase === null) {
return;
}
if ($test instanceof SelfDescribing) {
$buffer = $test->toString() . "\n";
} else {
$buffer = '';
}
$buffer .= TestFailure::exceptionToString($t) . "\n" .
Filter::getFilteredStacktrace($t);
$fault = $this->document->createElement(
$type,
Xml::prepareString($buffer)
);
if ($t instanceof ExceptionWrapper) {
$fault->setAttribute('type', $t->getClassName());
} else {
$fault->setAttribute('type', \get_class($t));
}
$this->currentTestCase->appendChild($fault);
}
private function doAddSkipped(Test $test): void
{
if ($this->currentTestCase === null) {
return;
}
$skipped = $this->document->createElement('skipped');
$this->currentTestCase->appendChild($skipped);
$this->testSuiteSkipped[$this->testSuiteLevel]++;
}
}
src/Util/Log/TeamCity.php 0000666 00000025715 13436751637 0011256 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\Log;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExceptionWrapper;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestFailure;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\ResultPrinter;
use PHPUnit\Util\Filter;
use ReflectionClass;
use SebastianBergmann\Comparator\ComparisonFailure;
/**
* A TestListener that generates a logfile of the test execution using the
* TeamCity format (for use with PhpStorm, for instance).
*/
class TeamCity extends ResultPrinter
{
/**
* @var bool
*/
private $isSummaryTestCountPrinted = false;
/**
* @var string
*/
private $startedTestName;
/**
* @var false|int
*/
private $flowId;
public function printResult(TestResult $result): void
{
$this->printHeader();
$this->printFooter($result);
}
/**
* An error occurred.
*
* @throws \InvalidArgumentException
*/
public function addError(Test $test, \Throwable $t, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$this->printEvent(
'testFailed',
[
'name' => $test->getName(),
'message' => self::getMessage($t),
'details' => self::getDetails($t),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* A warning occurred.
*
* @throws \InvalidArgumentException
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$this->printEvent(
'testFailed',
[
'name' => $test->getName(),
'message' => self::getMessage($e),
'details' => self::getDetails($e),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* A failure occurred.
*
* @throws \InvalidArgumentException
*/
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$parameters = [
'name' => $test->getName(),
'message' => self::getMessage($e),
'details' => self::getDetails($e),
'duration' => self::toMilliseconds($time),
];
if ($e instanceof ExpectationFailedException) {
$comparisonFailure = $e->getComparisonFailure();
if ($comparisonFailure instanceof ComparisonFailure) {
$expectedString = $comparisonFailure->getExpectedAsString();
if ($expectedString === null || empty($expectedString)) {
$expectedString = self::getPrimitiveValueAsString($comparisonFailure->getExpected());
}
$actualString = $comparisonFailure->getActualAsString();
if ($actualString === null || empty($actualString)) {
$actualString = self::getPrimitiveValueAsString($comparisonFailure->getActual());
}
if ($actualString !== null && $expectedString !== null) {
$parameters['type'] = 'comparisonFailure';
$parameters['actual'] = $actualString;
$parameters['expected'] = $expectedString;
}
}
}
$this->printEvent('testFailed', $parameters);
}
/**
* Incomplete test.
*/
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$this->printIgnoredTest($test->getName(), $t, $time);
}
/**
* Risky test.
*
* @throws \InvalidArgumentException
*/
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$this->addError($test, $t, $time);
}
/**
* Skipped test.
*
* @throws \ReflectionException
*/
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
$testName = $test->getName();
if ($this->startedTestName !== $testName) {
$this->startTest($test);
$this->printIgnoredTest($testName, $t, $time);
$this->endTest($test, $time);
} else {
$this->printIgnoredTest($testName, $t, $time);
}
}
public function printIgnoredTest($testName, \Throwable $t, float $time): void
{
$this->printEvent(
'testIgnored',
[
'name' => $testName,
'message' => self::getMessage($t),
'details' => self::getDetails($t),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* A testsuite started.
*
* @throws \ReflectionException
*/
public function startTestSuite(TestSuite $suite): void
{
if (\stripos(\ini_get('disable_functions'), 'getmypid') === false) {
$this->flowId = \getmypid();
} else {
$this->flowId = false;
}
if (!$this->isSummaryTestCountPrinted) {
$this->isSummaryTestCountPrinted = true;
$this->printEvent(
'testCount',
['count' => \count($suite)]
);
}
$suiteName = $suite->getName();
if (empty($suiteName)) {
return;
}
$parameters = ['name' => $suiteName];
if (\class_exists($suiteName, false)) {
$fileName = self::getFileName($suiteName);
$parameters['locationHint'] = "php_qn://$fileName::\\$suiteName";
} else {
$split = \explode('::', $suiteName);
if (\count($split) === 2 && \method_exists($split[0], $split[1])) {
$fileName = self::getFileName($split[0]);
$parameters['locationHint'] = "php_qn://$fileName::\\$suiteName";
$parameters['name'] = $split[1];
}
}
$this->printEvent('testSuiteStarted', $parameters);
}
/**
* A testsuite ended.
*/
public function endTestSuite(TestSuite $suite): void
{
$suiteName = $suite->getName();
if (empty($suiteName)) {
return;
}
$parameters = ['name' => $suiteName];
if (!\class_exists($suiteName, false)) {
$split = \explode('::', $suiteName);
if (\count($split) === 2 && \method_exists($split[0], $split[1])) {
$parameters['name'] = $split[1];
}
}
$this->printEvent('testSuiteFinished', $parameters);
}
/**
* A test started.
*
* @throws \ReflectionException
*/
public function startTest(Test $test): void
{
if (!$test instanceof TestCase) {
return;
}
$testName = $test->getName();
$this->startedTestName = $testName;
$params = ['name' => $testName];
if ($test instanceof TestCase) {
$className = \get_class($test);
$fileName = self::getFileName($className);
$params['locationHint'] = "php_qn://$fileName::\\$className::$testName";
}
$this->printEvent('testStarted', $params);
}
/**
* A test ended.
*/
public function endTest(Test $test, float $time): void
{
if (!$test instanceof TestCase) {
return;
}
parent::endTest($test, $time);
$this->printEvent(
'testFinished',
[
'name' => $test->getName(),
'duration' => self::toMilliseconds($time),
]
);
}
protected function writeProgress(string $progress): void
{
}
/**
* @param string $eventName
* @param array $params
*/
private function printEvent($eventName, $params = []): void
{
$this->write("\n##teamcity[$eventName");
if ($this->flowId) {
$params['flowId'] = $this->flowId;
}
foreach ($params as $key => $value) {
$escapedValue = self::escapeValue($value);
$this->write(" $key='$escapedValue'");
}
$this->write("]\n");
}
private static function getMessage(\Throwable $t): string
{
$message = '';
if ($t instanceof ExceptionWrapper) {
if ($t->getClassName() !== '') {
$message .= $t->getClassName();
}
if ($message !== '' && $t->getMessage() !== '') {
$message .= ' : ';
}
}
return $message . $t->getMessage();
}
/**
* @throws \InvalidArgumentException
*/
private static function getDetails(\Throwable $t): string
{
$stackTrace = Filter::getFilteredStacktrace($t);
$previous = $t instanceof ExceptionWrapper ? $t->getPreviousWrapped() : $t->getPrevious();
while ($previous) {
$stackTrace .= "\nCaused by\n" .
TestFailure::exceptionToString($previous) . "\n" .
Filter::getFilteredStacktrace($previous);
$previous = $previous instanceof ExceptionWrapper ?
$previous->getPreviousWrapped() : $previous->getPrevious();
}
return ' ' . \str_replace("\n", "\n ", $stackTrace);
}
private static function getPrimitiveValueAsString($value): ?string
{
if ($value === null) {
return 'null';
}
if (\is_bool($value)) {
return $value === true ? 'true' : 'false';
}
if (\is_scalar($value)) {
return \print_r($value, true);
}
return null;
}
private static function escapeValue(string $text): string
{
return \str_replace(
['|', "'", "\n", "\r", ']', '['],
['||', "|'", '|n', '|r', '|]', '|['],
$text
);
}
/**
* @param string $className
*
* @throws \ReflectionException
*/
private static function getFileName($className): string
{
$reflectionClass = new ReflectionClass($className);
return $reflectionClass->getFileName();
}
/**
* @param float $time microseconds
*/
private static function toMilliseconds(float $time): int
{
return \round($time * 1000);
}
}
src/Util/InvalidArgumentHelper.php 0000666 00000001677 13436751637 0013250 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PHPUnit\Framework\Exception;
/**
* Factory for PHPUnit\Framework\Exception objects that are used to describe
* invalid arguments passed to a function or method.
*/
final class InvalidArgumentHelper
{
public static function factory(int $argument, string $type, $value = null): Exception
{
$stack = \debug_backtrace();
return new Exception(
\sprintf(
'Argument #%d%sof %s::%s() must be a %s',
$argument,
$value !== null ? ' (' . \gettype($value) . '#' . $value . ')' : ' (No Value) ',
$stack[1]['class'],
$stack[1]['function'],
$type
)
);
}
}
src/Util/Filter.php 0000666 00000004705 13436751637 0010237 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\SyntheticError;
final class Filter
{
public static function getFilteredStacktrace(\Throwable $t): string
{
$prefix = false;
$script = \realpath($GLOBALS['_SERVER']['SCRIPT_NAME']);
if (\defined('__PHPUNIT_PHAR_ROOT__')) {
$prefix = __PHPUNIT_PHAR_ROOT__;
}
$filteredStacktrace = '';
if ($t instanceof SyntheticError) {
$eTrace = $t->getSyntheticTrace();
$eFile = $t->getSyntheticFile();
$eLine = $t->getSyntheticLine();
} elseif ($t instanceof Exception) {
$eTrace = $t->getSerializableTrace();
$eFile = $t->getFile();
$eLine = $t->getLine();
} else {
if ($t->getPrevious()) {
$t = $t->getPrevious();
}
$eTrace = $t->getTrace();
$eFile = $t->getFile();
$eLine = $t->getLine();
}
if (!self::frameExists($eTrace, $eFile, $eLine)) {
\array_unshift(
$eTrace,
['file' => $eFile, 'line' => $eLine]
);
}
$blacklist = new Blacklist;
foreach ($eTrace as $frame) {
if (isset($frame['file']) && \is_file($frame['file']) &&
(empty($GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST']) || !\in_array($frame['file'], $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'])) &&
!$blacklist->isBlacklisted($frame['file']) &&
($prefix === false || \strpos($frame['file'], $prefix) !== 0) &&
$frame['file'] !== $script) {
$filteredStacktrace .= \sprintf(
"%s:%s\n",
$frame['file'],
$frame['line'] ?? '?'
);
}
}
return $filteredStacktrace;
}
private static function frameExists(array $trace, string $file, int $line): bool
{
foreach ($trace as $frame) {
if (isset($frame['file']) && $frame['file'] === $file &&
isset($frame['line']) && $frame['line'] === $line) {
return true;
}
}
return false;
}
}
src/Util/TestResultCacheInterface.php 0000666 00000000715 13436751637 0013672 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner;
interface TestResultCacheInterface
{
public function getState($testName): int;
public function getTime($testName): float;
public function load(): void;
public function persist(): void;
}
src/Util/FileLoader.php 0000666 00000004113 13436751637 0011011 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PHPUnit\Framework\Exception;
/**
* Utility methods to load PHP sourcefiles.
*/
final class FileLoader
{
/**
* Checks if a PHP sourcecode file is readable. The sourcecode file is loaded through the load() method.
*
* As a fallback, PHP looks in the directory of the file executing the stream_resolve_include_path function.
* We do not want to load the Test.php file here, so skip it if it found that.
* PHP prioritizes the include_path setting, so if the current directory is in there, it will first look in the
* current working directory.
*
* @throws Exception
*/
public static function checkAndLoad(string $filename): string
{
$includePathFilename = \stream_resolve_include_path($filename);
$localFile = __DIR__ . \DIRECTORY_SEPARATOR . $filename;
/**
* @see https://github.com/sebastianbergmann/phpunit/pull/2751
*/
$isReadable = @\fopen($includePathFilename, 'r') !== false;
if (!$includePathFilename || !$isReadable || $includePathFilename === $localFile) {
throw new Exception(
\sprintf('Cannot open file "%s".' . "\n", $filename)
);
}
self::load($includePathFilename);
return $includePathFilename;
}
/**
* Loads a PHP sourcefile.
*/
public static function load(string $filename): void
{
$oldVariableNames = \array_keys(\get_defined_vars());
include_once $filename;
$newVariables = \get_defined_vars();
$newVariableNames = \array_diff(\array_keys($newVariables), $oldVariableNames);
foreach ($newVariableNames as $variableName) {
if ($variableName !== 'oldVariableNames') {
$GLOBALS[$variableName] = $newVariables[$variableName];
}
}
}
}
src/Util/TextTestListRenderer.php 0000666 00000002203 13436751637 0013110 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase;
final class TextTestListRenderer
{
public function render(TestSuite $suite): string
{
$buffer = 'Available test(s):' . \PHP_EOL;
foreach (new \RecursiveIteratorIterator($suite->getIterator()) as $test) {
if ($test instanceof TestCase) {
$name = \sprintf(
'%s::%s',
\get_class($test),
\str_replace(' with data set ', '', $test->getName())
);
} elseif ($test instanceof PhptTestCase) {
$name = $test->getName();
} else {
continue;
}
$buffer .= \sprintf(
' - %s' . \PHP_EOL,
$name
);
}
return $buffer;
}
}
src/Util/XmlTestListRenderer.php 0000666 00000004627 13436751637 0012740 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase;
final class XmlTestListRenderer
{
public function render(TestSuite $suite): string
{
$writer = new \XMLWriter;
$writer->openMemory();
$writer->setIndent(true);
$writer->startDocument();
$writer->startElement('tests');
$currentTestCase = null;
foreach (new \RecursiveIteratorIterator($suite->getIterator()) as $test) {
if ($test instanceof TestCase) {
if (\get_class($test) !== $currentTestCase) {
if ($currentTestCase !== null) {
$writer->endElement();
}
$writer->startElement('testCaseClass');
$writer->writeAttribute('name', \get_class($test));
$currentTestCase = \get_class($test);
}
$writer->startElement('testCaseMethod');
$writer->writeAttribute('name', $test->getName(false));
$writer->writeAttribute('groups', \implode(',', $test->getGroups()));
if (!empty($test->getDataSetAsString(false))) {
$writer->writeAttribute(
'dataSet',
\str_replace(
' with data set ',
'',
$test->getDataSetAsString(false)
)
);
}
$writer->endElement();
} elseif ($test instanceof PhptTestCase) {
if ($currentTestCase !== null) {
$writer->endElement();
$currentTestCase = null;
}
$writer->startElement('phptFile');
$writer->writeAttribute('path', $test->getName());
$writer->endElement();
} else {
continue;
}
}
if ($currentTestCase !== null) {
$writer->endElement();
}
$writer->endElement();
return $writer->outputMemory();
}
}
src/Util/XdebugFilterScriptGenerator.php 0000666 00000002646 13436751637 0014434 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util;
final class XdebugFilterScriptGenerator
{
public function generate(array $filterData): string
{
$items = $this->getWhitelistItems($filterData);
$files = \array_map(
function ($item) {
return \sprintf(
" '%s'",
$item
);
},
$items
);
$files = \implode(",\n", $files);
return <<