From f9a32e1bfd9a77b09b630cecf335a143e94dd331 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 12:00:18 -0300 Subject: [PATCH 1/9] feat(support): Refactor codebase to improve performance. --- .editorconfig | 17 ++ .gitattributes | 32 ++- .github/CODE_OF_CONDUCT.md | 102 ------- .github/ISSUE_TEMPLATE.md | 14 - .github/PULL_REQUEST_TEMPLATE.md | 6 - .github/ecs.yml | 34 --- .github/workflows/build.yml | 26 +- .github/workflows/dependency-check.yml | 29 +- .github/workflows/ecs.yml | 21 ++ .github/workflows/linter.yml | 17 ++ .github/workflows/mutation.yml | 27 +- .github/workflows/static.yml | 29 +- .gitignore | 63 +++-- CHANGELOG.md | 5 +- LICENSE | 31 +++ LICENSE.md | 27 -- README.md | 251 +++++++++++++----- composer.json | 28 +- docs/development.md | 43 +++ docs/svgs/features-mobile.svg | 75 ++++++ docs/svgs/features.svg | 72 +++++ docs/testing.md | 67 +++-- ecs.php | 30 ++- infection.json5 | 12 + phpunit.xml.dist | 38 +-- rector.php | 10 +- runtime/.gitignore | 2 +- src/EnumDataGenerator.php | 88 ++++++ src/TestSupport.php | 116 ++++---- tests/EnumDataGeneratorTest.php | 150 +++++++++++ tests/Stub/TestEnum.php | 17 ++ .../Provider/EnumDataGeneratorProvider.php | 55 ++++ tests/Support/runtime/.gitignore | 0 tests/Support/runtime/.gitkeep | 0 tests/TestSupportTest.php | 56 ++-- 35 files changed, 1095 insertions(+), 495 deletions(-) delete mode 100644 .github/CODE_OF_CONDUCT.md delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/ecs.yml create mode 100644 .github/workflows/ecs.yml create mode 100644 .github/workflows/linter.yml create mode 100644 LICENSE delete mode 100644 LICENSE.md create mode 100644 docs/development.md create mode 100644 docs/svgs/features-mobile.svg create mode 100644 docs/svgs/features.svg create mode 100644 infection.json5 create mode 100644 src/EnumDataGenerator.php create mode 100644 tests/EnumDataGeneratorTest.php create mode 100644 tests/Stub/TestEnum.php create mode 100644 tests/Support/Provider/EnumDataGeneratorProvider.php create mode 100644 tests/Support/runtime/.gitignore create mode 100644 tests/Support/runtime/.gitkeep diff --git a/.editorconfig b/.editorconfig index 5e9a93e..518c149 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,8 +10,25 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true +[*.js] +indent_size = 2 + [*.md] trim_trailing_whitespace = false +[*.php] +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_after_type_cast = true + +[*.yaml] +indent_size = 2 + [*.yml] indent_size = 2 + +[*.xml.dist] +indent_size = 2 + +[LICENSE*] +indent_style = unset +indent_size = unset diff --git a/.gitattributes b/.gitattributes index 76da65a..fdf0ca4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -21,20 +21,24 @@ *.gif binary *.ttf binary -# Ignore some meta files when creating an archive of this repository -/.github export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.phpunit-watcher.yml export-ignore -/.scrutinizer.yml export-ignore -/.styleci.yml export-ignore -/infection.json.dist export-ignore -/phpunit.xml.dist export-ignore -/psalm.xml export-ignore -/tests export-ignore -/docs export-ignore - # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ /CHANGELOG.md merge=union + +# Exclude files from the archive +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.styleci.yml export-ignore +/codeception.yml export-ignore +/composer-require-checker.json export-ignore +/docs export-ignore +/ecs.php export-ignore +/infection.json* export-ignore +/phpstan*.neon* export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml export-ignore +/rector.php export-ignore +/runtime export-ignore +/tests export-ignore diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1946f00..0000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,102 +0,0 @@ -# Code of Conduct - -## Our Pledge - -As contributors and maintainers of this project, and in order to keep community open and welcoming, we ask to -respect all community members. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Enforcement Responsibilities - -Core team members are responsible for clarifying and enforcing our standards of acceptable behavior and will take -appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Core team members 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, and will communicate reasons for -moderation decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing -the community in public spaces. Examples of representing a project or community include using an official e-mail -address, posting via an official social media account, within project GitHub, official forum or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting core team members. All -complaints will be reviewed and investigated promptly and fairly. - -All core team members are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Core team members will follow these Community Impact Guidelines in determining the consequences for any action they -deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in -the community. - -**Consequence**: A private, written warning from core team members, providing clarity around the nature of the violation -and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including -unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding -interactions in community spaces as well as external channels like social media. Violating these terms may lead to -a temporary or permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified -period of time. No public or private interaction with the people involved, including unsolicited interaction with those -enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate -behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 7e17783..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -### What steps will reproduce the problem? - -### What is the expected result? - -### What do you get instead? - - -### Additional info - -| Q | A -| ---------------- | --- -| Version | 1.0.? -| PHP version | -| Operating system | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index cecccf6..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,6 +0,0 @@ -| Q | A -| ------------- | --- -| Is bugfix? | ✔️/❌ -| New feature? | ✔️/❌ -| Breaks BC? | ✔️/❌ -| Fixed issues | diff --git a/.github/ecs.yml b/.github/ecs.yml deleted file mode 100644 index dff0db6..0000000 --- a/.github/ecs.yml +++ /dev/null @@ -1,34 +0,0 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - - push: - branches: ['main'] - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - -name: ecs - -jobs: - easy-coding-standard: - uses: php-forge/actions/.github/workflows/ecs.yml@main - secrets: - AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} - with: - os: >- - ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b51512..3254d3d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,24 +1,22 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths name: build +permissions: + contents: read + jobs: phpunit: - uses: php-forge/actions/.github/workflows/phpunit.yml@v1 + uses: yii2-framework/actions/.github/workflows/phpunit.yml@main secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index 48115b8..e69d7d1 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -1,22 +1,21 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths + +name: Composer require checker -name: dependency-check +permissions: + contents: read + pull-requests: write jobs: composer-require-checker: - uses: php-forge/actions/.github/workflows/composer-require-checker.yml@v1 + uses: yii2-framework/actions/.github/workflows/composer-require-checker.yml@main diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml new file mode 100644 index 0000000..e18f15c --- /dev/null +++ b/.github/workflows/ecs.yml @@ -0,0 +1,21 @@ +--- +on: + pull_request: &ignore-paths + paths-ignore: + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" + + push: *ignore-paths + +name: ecs + +permissions: + contents: read + pull-requests: write + +jobs: + easy-coding-standard: + uses: yii2-framework/actions/.github/workflows/ecs.yml@main diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..7b46557 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,17 @@ +--- +on: + - pull_request + - push + +name: linter + +permissions: + checks: write + contents: read + statuses: write + +jobs: + linter: + uses: yii2-framework/actions/.github/workflows/super-linter.yml@v1 + secrets: + AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 091ee70..91ef2b9 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -1,25 +1,24 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths name: mutation test +permissions: + contents: read + pull-requests: write + jobs: mutation: - uses: php-forge/actions/.github/workflows/infection.yml@v1 + uses: yii2-framework/actions/.github/workflows/infection.yml@v1 with: phpstan: true secrets: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2e31d77..99d7c1c 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,22 +1,21 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths name: static analysis +permissions: + contents: read + pull-requests: write + jobs: - psalm: - uses: php-forge/actions/.github/workflows/phpstan.yml@v1 + phpstan: + uses: yii2-framework/actions/.github/workflows/phpstan.yml@main diff --git a/.gitignore b/.gitignore index bb7ff36..d4c5c11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,51 @@ -# phpstorm project files -.idea +# codecoverage (if present) +code_coverage -# netbeans project files -nbproject +# codeception (if present) +c3.php -# zend studio for eclipse project files -.buildpath -.project -.settings +# composer +composer.lock -# windows thumbnail cache -Thumbs.db +# gitHub copilot config (if present) +.github/agents/** +.github/copilot-instructions.md +.github/copilot/** +.github/instructions/** +.github/prompts/** +.github/skills/** -# Mac DS_Store Files +# mac ds_store (if present) .DS_Store -# composer vendor dir -/vendor +# netbeans project (if present) +nbproject -# composer lock file -/composer.lock +# node_modules (if present) +node_modules +package-lock.json -# composer itself is not needed -composer.phar +# phpstorm project (if present) +.idea -# phpunit itself is not needed -phpunit.phar +# phpunit (if present) +.phpunit.cache +.phpunit.result.cache +phpunit.xml* -# local phpunit config -/phpunit.xml +# vagrant (if present) +.vagrant -# phpunit cache -.phpunit.result.cache +# vendor +vendor + +# vscode project (if present) +.vscode + +# windows thumbnail cache (if present) +Thumbs.db + +# zend studio for eclipse project (if present) +.buildpath +.project +.settings diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc0acc..de22dc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -Change Log -========== +# ChangeLog ## 0.2.1 Under development +- Enh #13: Refactor codebase to improve performance (@terabytesoftw) + ## 0.2.0 August 18, 2025 - Bug #11: Refactor project structure and update dependencies (@terabytesoftw) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efb41e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +SPDX-License-Identifier: BSD-3-Clause + +BSD 3-Clause License + +Copyright (c) 2024, Terabytesoftw (https://github.com/terabytesoftw/) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder nor the names of its + 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 HOLDER 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. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index a76279a..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,27 +0,0 @@ -# BSD 3-Clause License - -Copyright © 2008 by Terabytesoftw () -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 PHP Forge (Terabytesoftw) nor the names of its - 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. diff --git a/README.md b/README.md index 7a2e317..b58db59 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,69 @@ +

- + PHP Forge -

Support utilities for enhanced testing capabilities.

+

Support


+

- - PHP Version - - PHPUnit + PHPUnit - Infection - - - Static Analysis - + Mutation Testing + + + PHPStan + +

+ +

+ Support utilities for PHPUnit-focused development
+ Reflection helpers, line ending normalization, and filesystem cleanup for deterministic tests.

## Features -✅ **Advanced Reflection Utilities** + + + Feature Overview + + +**Advanced Reflection Utilities** - Access and modify private and protected properties via reflection. - Invoke inaccessible methods to expand testing coverage. -✅ **Cross-Platform String Assertions** +**Cross-Platform String Assertions** - Avoid false positives and negatives caused by Windows vs. Unix line ending differences. - Normalize line endings for consistent string comparisons across platforms. -✅ **File System Test Management** +**File System Test Management** - Recursively clean files and directories for isolated test environments. -- Safe removal that preserves Git-tracking files (for example, '.gitignore', '.gitkeep'). - -## Quick start - -### System requirements +- Safe removal that preserves Git-tracking files (for example, `.gitignore`, `.gitkeep`). -- [`PHP`](https://www.php.net/downloads) 8.1 or higher. -- [`Composer`](https://getcomposer.org/download/) for dependency management. -- [`PHPUnit`](https://phpunit.de/) for testing framework integration. +**Enum Test Data Generation** +- Generate structured, deterministic datasets for enum-based attribute scenarios via `PHPForge\Support\EnumDataGenerator`. ### Installation -#### Method 1: Using [Composer](https://getcomposer.org/download/) (recommended) - -Install the extension. - ```bash -composer require --dev --prefer-dist php-forge/support:^0.2 -``` - -#### Method 2: Manual installation - -Add to your `composer.json`. - -```json -{ - "require-dev": { - "php-forge/support": "^0.2" - } -} +composer require php-forge/support:^0.2 --dev ``` -Then run. +### Quick start -```bash -composer update -``` +This package provides the `PHPForge\Support\TestSupport` trait for PHPUnit tests. -## Basic Usage +It supports reflection-based access to non-public members, deterministic string comparisons across platforms, and filesystem cleanup for isolated test environments. -### Accessing private properties +#### Accessing private properties ```php + + +
+ +
+
+

Advanced reflection utilities

+

Access and modify private or protected properties for controlled test setups.

+
+
+

Cross-platform string assertions

+

Normalize line endings to avoid Windows vs. Unix comparison differences.

+
+
+

Enum dataset generation

+

Generate deterministic datasets for enum-based attribute test cases via EnumDataGenerator.

+
+
+

Filesystem test management

+

Remove test artifacts while preserving Git-tracked placeholders such as .gitignore and .gitkeep.

+
+
+

Invoke inaccessible methods

+

Call non-public methods to verify behavior without changing production visibility.

+
+
+

PHPUnit-focused helpers

+

Use the TestSupport trait to share test utilities across the suite.

+
+
+
+
+
diff --git a/docs/svgs/features.svg b/docs/svgs/features.svg new file mode 100644 index 0000000..d971e80 --- /dev/null +++ b/docs/svgs/features.svg @@ -0,0 +1,72 @@ + + +
+ +
+
+

Advanced reflection utilities

+

Access and modify private or protected properties for controlled test setups.

+
+
+

Cross-platform string assertions

+

Normalize line endings to avoid Windows vs. Unix comparison differences.

+
+
+

Enum dataset generation

+

Generate deterministic datasets for enum-based attribute test cases via EnumDataGenerator.

+
+
+

Filesystem test management

+

Remove test artifacts while preserving Git-tracked placeholders such as .gitignore and .gitkeep.

+
+
+

Invoke inaccessible methods

+

Call non-public methods to verify behavior without changing production visibility.

+
+
+

PHPUnit-focused helpers

+

Use the TestSupport trait to share test utilities across the suite.

+
+
+
+
+
diff --git a/docs/testing.md b/docs/testing.md index 2838bf5..c3f1ca8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,50 +1,73 @@ # Testing -## Checking dependencies +This package provides a consistent set of [Composer](https://getcomposer.org/) scripts for local validation. -This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`. +Tool references: -To run the checker, execute the following command. +- [Composer Require Checker](https://github.com/maglnet/ComposerRequireChecker) for dependency definition checks. +- [Easy Coding Standard (ECS)](https://github.com/easy-coding-standard/easy-coding-standard) for coding standards. +- [Infection](https://infection.github.io/) for mutation testing. +- [PHPStan](https://phpstan.org/) for static analysis. +- [PHPUnit](https://phpunit.de/) for unit tests. -```shell -composer run check-dependencies +## Coding standards (ECS) + +Run Easy Coding Standard (ECS) and apply fixes. + +```bash +composer run ecs ``` -## Easy coding standard +## Dependency definition check -The code is checked with [Easy Coding Standard](https://github.com/easy-coding-standard/easy-coding-standard) and -[PHP CS Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer). To run it. +Verify that runtime dependencies are correctly declared in `composer.json`. -```shell -composer run ecs +```bash +composer run check-dependencies ``` -## Mutation testing +## Mutation testing (Infection) -Mutation testing is checked with [Infection](https://infection.github.io/). To run it. +Run mutation testing. -```shell +```bash composer run mutation ``` -With PHPStan analysis, it will also check for static analysis issues during mutation testing. +Run mutation testing with static analysis enabled. -```shell +```bash composer run mutation-static ``` -## Static analysis +## Static analysis (PHPStan) -The code is statically analyzed with [PHPStan](https://phpstan.org/). To run static analysis. +Run static analysis. -```shell +```bash composer run static ``` -## Unit Tests +## Unit tests (PHPUnit) + +Run the full test suite. + +```bash +composer run tests +``` + +## Passing extra arguments + +Composer scripts support forwarding additional arguments using `--`. + +Example: run a specific PHPUnit test or filter by name. + +```bash +composer run tests -- --filter SvgTest +``` -The code is tested with [PHPUnit](https://phpunit.de/). To run tests. +Example: run PHPStan with a different memory limit: -```shell -composer run test +```bash +composer run static -- --memory-limit=512M ``` diff --git a/ecs.php b/ecs.php index 00781d8..726b0dd 100644 --- a/ecs.php +++ b/ecs.php @@ -2,13 +2,11 @@ declare(strict_types=1); -use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer; -use PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer; -use PhpCsFixer\Fixer\ClassNotation\OrderedTraitsFixer; -use PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer; -use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer; -use PhpCsFixer\Fixer\Import\OrderedImportsFixer; +use PhpCsFixer\Fixer\ClassNotation\{ClassDefinitionFixer, OrderedClassElementsFixer, OrderedTraitsFixer}; +use PhpCsFixer\Fixer\Import\{NoUnusedImportsFixer, OrderedImportsFixer}; +use PhpCsFixer\Fixer\Phpdoc\PhpdocTypesOrderFixer; use PhpCsFixer\Fixer\StringNotation\SingleQuoteFixer; +use PhpCsFixer\Fixer\LanguageConstruct\NullableTypeDeclarationFixer; use Symplify\EasyCodingStandard\Config\ECSConfig; return ECSConfig::configure() @@ -33,7 +31,7 @@ 'construct', 'destruct', 'magic', - 'phpunit', + 'method_protected_abstract', 'method_public', 'method_protected', 'method_private', @@ -44,14 +42,19 @@ ->withConfiguredRule( OrderedImportsFixer::class, [ - 'imports_order' => ['class', 'function', 'const'], + 'imports_order' => [ + 'class', + 'function', + 'const', + ], 'sort_algorithm' => 'alpha', ], ) ->withConfiguredRule( - VisibilityRequiredFixer::class, + PhpdocTypesOrderFixer::class, [ - 'elements' => [], + 'sort_algorithm' => 'none', + 'null_adjustment' => 'always_last', ], ) ->withFileExtensions(['php']) @@ -61,7 +64,7 @@ __DIR__ . '/tests', ], ) - ->withPhpCsFixerSets(perCS20: true) + ->withPhpCsFixerSets(perCS30: true) ->withPreparedSets( cleanCode: true, comments: true, @@ -75,4 +78,9 @@ OrderedTraitsFixer::class, SingleQuoteFixer::class, ] + ) + ->withSkip( + [ + NullableTypeDeclarationFixer::class, + ] ); diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..1d33ec4 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,12 @@ +{ + $schema: "./vendor/infection/infection/resources/schema.json", + logs: { + text: "php://stderr", + stryker: { + report: "main", + }, + }, + source: { + directories: ["src"], + }, +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e5d7313..521d51e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,24 +1,24 @@ - - - tests - - + + + tests + + - - - ./src - - + + + ./src + + diff --git a/rector.php b/rector.php index 4a7a938..2225bf2 100644 --- a/rector.php +++ b/rector.php @@ -2,7 +2,7 @@ declare(strict_types=1); -return static function (Rector\Config\RectorConfig $rectorConfig): void { +return static function (\Rector\Config\RectorConfig $rectorConfig): void { $rectorConfig->parallel(); $rectorConfig->importNames(); @@ -16,13 +16,13 @@ $rectorConfig->sets( [ - Rector\Set\ValueObject\SetList::PHP_81, - Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81, - Rector\Set\ValueObject\SetList::TYPE_DECLARATION, + \Rector\Set\ValueObject\SetList::PHP_81, + \Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81, + \Rector\Set\ValueObject\SetList::TYPE_DECLARATION, ], ); $rectorConfig->rule( - Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class + \Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class ); }; diff --git a/runtime/.gitignore b/runtime/.gitignore index d6b7ef3..c96a04f 100644 --- a/runtime/.gitignore +++ b/runtime/.gitignore @@ -1,2 +1,2 @@ * -!.gitignore +!.gitignore \ No newline at end of file diff --git a/src/EnumDataGenerator.php b/src/EnumDataGenerator.php new file mode 100644 index 0000000..e6c8134 --- /dev/null +++ b/src/EnumDataGenerator.php @@ -0,0 +1,88 @@ + $enumClass Enum class name implementing UnitEnum. + * @param string $enumClass Enum class name implementing UnitEnum. + * @param string|UnitEnum $attribute Attribute name used to build the expected fragment. + * @param bool $asHtml Whether to generate expected output as an attribute fragment or enum instance. Default is `true`. + * + * @return array Structured test cases indexed by a normalized enum value key. + * + * @phpstan-return array + */ + public static function cases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array + { + $cases = []; + $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute)); + + foreach ($enumClass::cases() as $case) { + $normalizedValue = Enum::normalizeValue($case); + + $key = "enum: {$normalizedValue}"; + $expected = $asHtml ? " {$attributeName}=\"{$normalizedValue}\"" : $case; + $message = $asHtml + ? "Should return the '{$attributeName}' attribute value for enum case: {$normalizedValue}." + : "Should return the enum instance for case: {$normalizedValue}."; + + $cases[$key] = [ + $case, + [], + $expected, + $message, + ]; + } + + return $cases; + } + + /** + * Generates test cases for tag-related enum scenarios. + * + * Produces a dataset mapping descriptive keys to enum cases and their normalized string values, suitable for + * data provider methods in PHPUnit tests. + * + * @phpstan-param class-string $enumClass Enum class name implementing UnitEnum. + * @param string $enumClass Enum class name implementing UnitEnum. + * @param string $category Category label appended to the generated keys. + * + * @phpstan-return array Structured test cases indexed by descriptive keys. + */ + public static function tagCases(string $enumClass, string $category): array + { + $data = []; + + foreach ($enumClass::cases() as $case) { + $value = (string) Enum::normalizeValue($case); + $data[sprintf('%s %s tag', $value, $category)] = [$case, $value]; + } + + return $data; + } +} diff --git a/src/TestSupport.php b/src/TestSupport.php index 567c9b2..628755b 100644 --- a/src/TestSupport.php +++ b/src/TestSupport.php @@ -20,14 +20,13 @@ use function unlink; /** - * Trait providing utilities for testing inaccessible properties, methods, and filesystem cleanup. + * Test support utilities for PHPUnit suites. * - * Supplies static helper methods for normalizing line endings, accessing or modifying private/protected properties and - * methods (including those inherited from parent classes), invoking inaccessible methods, and recursively removing - * files from directories. + * Provides reflection-based helpers to read and write non-public properties and invoke non-public methods, including + * members declared on parent classes. * - * These utilities are designed to facilitate comprehensive unit testing by enabling assertions and manipulations that - * would otherwise be restricted by visibility constraints or platform differences. + * Also provides helpers for normalizing line endings to `"\n"` and for removing files from directories while + * preserving `.gitignore` and `.gitkeep`. * * @copyright Copyright (C) 2025 Terabytesoftw. * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License. @@ -35,25 +34,21 @@ trait TestSupport { /** - * Retrieves the value of an inaccessible property from a parent class instance. + * Retrieves a property value from a specified class context. * - * Uses reflection to access the specified property of the given parent class, allowing tests to inspect or assert - * the value of private or protected properties inherited from parent classes. - * - * This method is useful for verifying the internal state of objects in inheritance scenarios where direct access - * to parent properties is not possible. + * Uses reflection to read `$propertyName` from `$object` using `$className` as the declaring class context. * * @param object $object Object instance from which to retrieve the property value. - * @param object|string $className Name or instance of the parent class containing the property. + * @param object|string $className Name or instance of the class that declares the property. * @param string $propertyName Name of the property to access. * * @throws ReflectionException * - * @return mixed Value of the specified parent property. + * @return mixed Value of the specified property. * * @phpstan-param class-string|object $className */ - public static function inaccessibleParentProperty( + private static function inaccessibleParentProperty( object $object, string|object $className, string $propertyName, @@ -64,24 +59,20 @@ public static function inaccessibleParentProperty( } /** - * Retrieves the value of an inaccessible property from an object or class instance. - * - * Uses reflection to access the specified property of the given object or class, allowing tests to inspect or - * assert the value of private or protected properties that are otherwise inaccessible. + * Retrieves a property value from a class or object. * - * This method is useful for verifying the internal state of objects during testing, especially when direct access - * to the property is not possible due to visibility constraints. + * Uses reflection to read `$propertyName` from the provided class name or object instance. * * @param object|string $object Name of the class or object instance from which to retrieve the property value. * @param string $propertyName Name of the property to access. * - * @throws ReflectionException if the property does not exist or is inaccessible. + * @throws ReflectionException if the property does not exist. * - * @return mixed Value of the specified property, or `null` if the property name is empty. + * @return mixed Value of the specified property, or `null` when `$propertyName` is empty. * * @phpstan-param class-string|object $object */ - public static function inaccessibleProperty(string|object $object, string $propertyName): mixed + private static function inaccessibleProperty(string|object $object, string $propertyName): mixed { $class = new ReflectionClass($object); @@ -94,24 +85,21 @@ public static function inaccessibleProperty(string|object $object, string $prope } /** - * Invokes an inaccessible method on the given object instance with the specified arguments. + * Invokes a method via reflection. * - * Uses reflection to access and invoke a private or protected method of the provided object, allowing tests to - * execute logic that is not publicly accessible. - * - * This is useful for verifying internal behavior or side effects during unit testing. + * Uses reflection to invoke `$method` on `$object` with `$args`. * * @param object $object Object instance containing the method to invoke. * @param string $method Name of the method to invoke. * @param array $args Arguments to pass to the method invocation. * - * @throws ReflectionException if the method does not exist or is inaccessible. + * @throws ReflectionException if the method does not exist. * - * @return mixed Value of the invoked method, or `null` if the method name is empty. + * @return mixed Value of the invoked method, or `null` when `$method` is empty. * * @phpstan-param array $args */ - public static function invokeMethod(object $object, string $method, array $args = []): mixed + private static function invokeMethod(object $object, string $method, array $args = []): mixed { $reflection = new ReflectionObject($object); @@ -124,26 +112,23 @@ public static function invokeMethod(object $object, string $method, array $args } /** - * Invokes an inaccessible method from a parent class on the given object instance with the specified arguments. - * - * Uses reflection to access and invoke a private or protected method defined in the specified parent class, - * allowing tests to execute logic that is not publicly accessible from the child class. + * Invokes a parent class method via reflection. * - * This is useful for verifying inherited behavior or side effects during unit testing of subclasses. + * Uses `$parentClass` as the declaring class context to invoke `$method` on `$object` with `$args`. * * @param object $object Object instance containing the method to invoke. * @param string $parentClass Name of the parent class containing the method. * @param string $method Name of the method to invoke. * @param array $args Arguments to pass to the method invocation. * - * @throws ReflectionException if the method does not exist or is inaccessible in the parent class. + * @throws ReflectionException if the method does not exist. * - * @return mixed Value of the invoked method, or `null` if the method name is empty. + * @return mixed Value of the invoked method, or `null` when `$method` is empty. * * @phpstan-param class-string $parentClass * @phpstan-param array $args */ - public static function invokeParentMethod( + private static function invokeParentMethod( object $object, string $parentClass, string $method, @@ -160,36 +145,32 @@ public static function invokeParentMethod( } /** - * Normalizes line endings to Unix style ('\n') for cross-platform string assertions. + * Normalizes line endings to `"\n"` for cross-platform string assertions. * - * Converts Windows style ('\r\n') line endings to Unix style ('\n') to ensure consistent string comparisons across - * different operating systems during testing. - * - * This method is useful for eliminating false negatives in assertions caused by platform-specific line endings. + * Converts `"\r\n"` and `"\r"` to `"\n"` to keep string comparisons deterministic across operating systems. * * @param string $line Input string potentially containing Windows style line endings. * - * @return string String with normalized Unix style line endings. + * @return string String with normalized line endings. */ - public static function normalizeLineEndings(string $line): string + private static function normalizeLineEndings(string $line): string { return str_replace(["\r\n", "\r"], "\n", $line); } /** - * Removes all files and directories recursively from the specified base path, excluding '.gitignore' and - * '.gitkeep'. + * Removes directory contents recursively while preserving `.gitignore` and `.gitkeep`. * - * Opens the given directory, iterates through its contents, and removes all files and subdirectories except for - * special entries ('.', '..', '.gitignore', '.gitkeep'). + * Iterates through `$basePath` and removes all files and subdirectories except `.`, `..`, `.gitignore`, and + * `.gitkeep`. Subdirectories are processed recursively before removal. * - * Subdirectories are processed recursively before removal. + * File and directory removals are attempted with error suppression. * * @param string $basePath Absolute path to the directory whose contents will be removed. * * @throws RuntimeException if the directory cannot be opened for reading. */ - public static function removeFilesFromDirectory(string $basePath): void + private static function removeFilesFromDirectory(string $basePath): void { $handle = @opendir($basePath); @@ -199,7 +180,11 @@ public static function removeFilesFromDirectory(string $basePath): void } while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..' || $file === '.gitignore' || $file === '.gitkeep') { + if ($file === '.' || $file === '..') { + continue; + } + + if ($file === '.gitignore' || $file === '.gitkeep') { continue; } @@ -217,24 +202,22 @@ public static function removeFilesFromDirectory(string $basePath): void } /** - * Sets the value of an inaccessible property on a parent class instance. + * Sets a parent class property value via reflection. * - * Uses reflection to assign the specified value to a private or protected property defined in the given parent - * class, enabling tests to modify internal state that is otherwise inaccessible due to visibility constraints in - * inheritance scenarios. + * Uses `$parentClass` as the declaring class context to assign `$value` to `$propertyName` on `$object`. * - * This method is useful for testing scenarios that require direct manipulation of parent class internals. + * This method is a no-op when `$propertyName` is empty. * * @param object $object Object instance whose parent property will be set. * @param string $parentClass Name of the parent class containing the property. * @param string $propertyName Name of the property to set. * @param mixed $value Value to assign to the property. * - * @throws ReflectionException if the property does not exist or is inaccessible in the parent class. + * @throws ReflectionException if the property does not exist. * * @phpstan-param class-string $parentClass */ - public static function setInaccessibleParentProperty( + private static function setInaccessibleParentProperty( object $object, string $parentClass, string $propertyName, @@ -251,20 +234,19 @@ public static function setInaccessibleParentProperty( } /** - * Sets the value of an inaccessible property on the given object instance. + * Sets a property value via reflection. * - * Uses reflection to assign the specified value to a private or protected property of the provided object enabling - * tests to modify internal state that is otherwise inaccessible due to visibility constraints. + * Assigns `$value` to `$propertyName` on `$object`. * - * This method is useful for testing scenarios that require direct manipulation of object internals. + * This method is a no-op when `$propertyName` is empty. * * @param object $object Object instance whose property will be set. * @param string $propertyName Name of the property to set. * @param mixed $value Value to assign to the property. * - * @throws ReflectionException if the property does not exist or is inaccessible. + * @throws ReflectionException if the property does not exist. */ - public static function setInaccessibleProperty( + private static function setInaccessibleProperty( object $object, string $propertyName, mixed $value, diff --git a/tests/EnumDataGeneratorTest.php b/tests/EnumDataGeneratorTest.php new file mode 100644 index 0000000..c7d2a10 --- /dev/null +++ b/tests/EnumDataGeneratorTest.php @@ -0,0 +1,150 @@ + $enumClass + */ + #[DataProviderExternal(EnumDataGeneratorProvider::class, 'casesParameters')] + public function testCasesGenerateExpectedStructure( + string $enumClass, + string|UnitEnum $attribute, + bool $asHtml, + ): void { + $data = match ($asHtml) { + true => EnumDataGenerator::cases($enumClass, $attribute), + false => EnumDataGenerator::cases($enumClass, $attribute, false), + }; + + self::assertNotEmpty($data, 'Should return at least one data set.'); + + $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute)); + + foreach ($enumClass::cases() as $case) { + $expectedKey = 'enum: ' . Enum::normalizeValue($case); + + self::assertArrayHasKey( + $expectedKey, + $data, + "Should include a dataset key for enum case '{$case->name}'.", + ); + + $row = $data[$expectedKey] ?? null; + + self::assertIsArray( + $row, + "Should return a dataset array for enum case '{$case->name}'.", + ); + self::assertCount( + 4, + $row, + "Should return a 4-tuple dataset for enum case '{$case->name}'.", + ); + + /** @var array{UnitEnum, array, string|UnitEnum, string} $row */ + [$value, $attributes, $expected, $message] = $row; + + self::assertSame( + $case, + $value, + "Should store the enum case instance for '{$case->name}'.", + ); + self::assertSame( + [], + $attributes, + "Should store an empty attributes array for '{$case->name}'.", + ); + + $expectedValue = Enum::normalizeValue($case); + + if ($asHtml) { + self::assertSame( + " {$attributeName}=\"{$expectedValue}\"", + $expected, + "Should generate expected attribute fragment for '{$case->name}'.", + ); + $expectedMessage = "Should return the '{$attributeName}' attribute value for enum case: {$expectedValue}."; + } else { + self::assertSame( + $case, + $expected, + "Should return the enum instance as expected output for '{$case->name}'.", + ); + $expectedMessage = "Should return the enum instance for case: {$expectedValue}."; + } + + self::assertSame( + $expectedMessage, + $message, + "Should store the expected message for '{$case->name}'.", + ); + } + } + + /** + * @phpstan-param class-string $enumClass + */ + #[DataProviderExternal(EnumDataGeneratorProvider::class, 'tagCasesParameters')] + public function testTagCasesGenerateExpectedStructure(string $enumClass, string $category): void + { + $data = EnumDataGenerator::tagCases($enumClass, $category); + + self::assertNotEmpty($data, 'Should return at least one data set.'); + + foreach ($enumClass::cases() as $case) { + $expectedKey = Enum::normalizeValue($case) . " {$category} tag"; + + self::assertArrayHasKey( + $expectedKey, + $data, + "Should include a dataset key for enum case '{$case->name}'.", + ); + + $row = $data[$expectedKey] ?? null; + + self::assertIsArray( + $row, + "Should return a dataset array for enum case '{$case->name}'.", + ); + self::assertCount( + 2, + $row, + "Should return a 2-tuple dataset for enum case '{$case->name}'.", + ); + + /** @var array{UnitEnum, string} $row */ + [$value, $normalizedValue] = $row; + + self::assertSame( + $case, + $value, + "Should store the enum case instance for '{$case->name}'.", + ); + self::assertSame( + Enum::normalizeValue($case), + $normalizedValue, + "Should store normalized value for '{$case->name}'.", + ); + } + } +} diff --git a/tests/Stub/TestEnum.php b/tests/Stub/TestEnum.php new file mode 100644 index 0000000..7576e60 --- /dev/null +++ b/tests/Stub/TestEnum.php @@ -0,0 +1,17 @@ + + */ + public static function casesParameters(): array + { + return [ + 'as html' => [ + TestEnum::class, + 'data-test', + true, + ], + 'as enum instance' => [ + TestEnum::class, + 'data-test', + false, + ], + 'attribute as enum instance' => [ + TestEnum::class, + TestEnum::Foo, + true, + ], + ]; + } + + /** + * @return array + */ + public static function tagCasesParameters(): array + { + return [ + 'default category' => [ + TestEnum::class, + 'element', + ], + ]; + } +} diff --git a/tests/Support/runtime/.gitignore b/tests/Support/runtime/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/Support/runtime/.gitkeep b/tests/Support/runtime/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php index 5fd41d5..fd8a534 100644 --- a/tests/TestSupportTest.php +++ b/tests/TestSupportTest.php @@ -50,33 +50,59 @@ public function testInaccessibleParentPropertyReturnsExpectedValue(): void ); } + public function testNormalizeLineEndingsReturnsUnchangedStringWhenNoNormalizationNeeded(): void + { + self::assertSame( + "foo\nbar", + self::normalizeLineEndings("foo\nbar"), + 'Should return the original string when no normalization is required.', + ); + } + public function testNormalizeLineEndingsWhenStringsAreIdenticalWithLineEndings(): void { self::assertSame( + "foo\nbar", self::normalizeLineEndings("foo\r\nbar"), - self::normalizeLineEndings("foo\nbar"), - "Should produce the same normalized string for 'CRLF' and 'LF' inputs.", + "Should normalize 'CRLF' inputs to 'LF'.", ); } public function testRemoveFilesFromDirectoryRemovesAllFiles(): void { - $dir = dirname(__DIR__) . '/runtime'; + $dir = __DIR__ . '/Support/runtime'; + if (is_dir($dir)) { + unlink("{$dir}/.gitignore"); + unlink("{$dir}/.gitkeep"); + rmdir($dir); + } + + mkdir($dir, 0777, true); mkdir("{$dir}/subdir"); touch("{$dir}/test.txt"); touch("{$dir}/subdir/test.txt"); + touch("{$dir}/.gitignore"); + touch("{$dir}/.gitkeep"); self::removeFilesFromDirectory($dir); - $this->assertFileDoesNotExist( + self::assertFileDoesNotExist( "{$dir}/test.txt", "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.", ); - $this->assertFileDoesNotExist( + self::assertFileDoesNotExist( "{$dir}/subdir/test.txt", "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.", ); + self::assertFileExists( + "{$dir}/.gitignore", + "File '.gitignore' should remain after 'removeFilesFromDirectory' method is called.", + ); + self::assertFileExists( + "{$dir}/.gitkeep", + "File '.gitkeep' should remain after 'removeFilesFromDirectory' method is called.", + ); } /** @@ -96,11 +122,11 @@ public function testReturnInaccessiblePropertyValueWhenPropertyIsPrivate(): void */ public function testReturnValueWhenInvokingInaccessibleMethod(): void { - $this->assertSame( + self::assertSame( 'value', self::invokeMethod(new TestClass(), 'inaccessibleMethod'), - "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " . - 'via reflection.', + "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " + . 'via reflection.', ); } @@ -109,15 +135,15 @@ public function testReturnValueWhenInvokingInaccessibleMethod(): void */ public function testReturnValueWhenInvokingInaccessibleParentMethod(): void { - $this->assertSame( + self::assertSame( 'valueParent', self::invokeParentMethod( new TestClass(), TestBaseClass::class, 'inaccessibleParentMethod', ), - "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " . - "'TestClass' via reflection.", + "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " + . "'TestClass' via reflection.", ); } @@ -130,11 +156,11 @@ public function testSetInaccessibleParentProperty(): void self::setInaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent', 'foo'); - $this->assertSame( + self::assertSame( 'foo', self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'), - "Should return 'foo' after setting the parent property 'propertyParent' via " . - "'setInaccessibleParentProperty' method.", + "Should return 'foo' after setting the parent property 'propertyParent' via " + . "'setInaccessibleParentProperty' method.", ); } @@ -147,7 +173,7 @@ public function testSetInaccessiblePropertySetsValueCorrectly(): void self::setInaccessibleProperty($object, 'property', 'foo'); - $this->assertSame( + self::assertSame( 'foo', self::inaccessibleProperty($object, 'property'), "Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.", From 4911dbc93586162acd04b7f57cc4a5e8c2034aec Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 19 Jan 2026 15:00:36 +0000 Subject: [PATCH 2/9] Apply fixes from StyleCI --- rector.php | 10 +++++----- src/EnumDataGenerator.php | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rector.php b/rector.php index 2225bf2..4a7a938 100644 --- a/rector.php +++ b/rector.php @@ -2,7 +2,7 @@ declare(strict_types=1); -return static function (\Rector\Config\RectorConfig $rectorConfig): void { +return static function (Rector\Config\RectorConfig $rectorConfig): void { $rectorConfig->parallel(); $rectorConfig->importNames(); @@ -16,13 +16,13 @@ $rectorConfig->sets( [ - \Rector\Set\ValueObject\SetList::PHP_81, - \Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81, - \Rector\Set\ValueObject\SetList::TYPE_DECLARATION, + Rector\Set\ValueObject\SetList::PHP_81, + Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81, + Rector\Set\ValueObject\SetList::TYPE_DECLARATION, ], ); $rectorConfig->rule( - \Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class + Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class ); }; diff --git a/src/EnumDataGenerator.php b/src/EnumDataGenerator.php index e6c8134..aca3ff8 100644 --- a/src/EnumDataGenerator.php +++ b/src/EnumDataGenerator.php @@ -29,6 +29,7 @@ final class EnumDataGenerator * `true`) or the enum case instance (when `$asHtml` is `false`). * * @phpstan-param class-string $enumClass Enum class name implementing UnitEnum. + * * @param string $enumClass Enum class name implementing UnitEnum. * @param string|UnitEnum $attribute Attribute name used to build the expected fragment. * @param bool $asHtml Whether to generate expected output as an attribute fragment or enum instance. Default is `true`. @@ -69,6 +70,7 @@ public static function cases(string $enumClass, string|UnitEnum $attribute, bool * data provider methods in PHPUnit tests. * * @phpstan-param class-string $enumClass Enum class name implementing UnitEnum. + * * @param string $enumClass Enum class name implementing UnitEnum. * @param string $category Category label appended to the generated keys. * From d80918bf28edc1df4c7586a381b5abb982290486 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 12:07:29 -0300 Subject: [PATCH 3/9] Fix Linter test. --- .github/linters/actionlint.yml | 7 +++++++ README.md | 15 --------------- runtime/.gitignore | 2 +- 3 files changed, 8 insertions(+), 16 deletions(-) create mode 100644 .github/linters/actionlint.yml diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml new file mode 100644 index 0000000..785d850 --- /dev/null +++ b/.github/linters/actionlint.yml @@ -0,0 +1,7 @@ +--- +paths: + .github/workflows/**/*.yml: + ignore: + - '"pull_request" section is alias node but mapping node is expected' + - '"push" section is alias node but mapping node is expected' + - "section is alias node but mapping node is expected" diff --git a/README.md b/README.md index b58db59..1be003b 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,6 @@ Feature Overview -**Advanced Reflection Utilities** -- Access and modify private and protected properties via reflection. -- Invoke inaccessible methods to expand testing coverage. - -**Cross-Platform String Assertions** -- Avoid false positives and negatives caused by Windows vs. Unix line ending differences. -- Normalize line endings for consistent string comparisons across platforms. - -**File System Test Management** -- Recursively clean files and directories for isolated test environments. -- Safe removal that preserves Git-tracking files (for example, `.gitignore`, `.gitkeep`). - -**Enum Test Data Generation** -- Generate structured, deterministic datasets for enum-based attribute scenarios via `PHPForge\Support\EnumDataGenerator`. - ### Installation ```bash diff --git a/runtime/.gitignore b/runtime/.gitignore index c96a04f..d6b7ef3 100644 --- a/runtime/.gitignore +++ b/runtime/.gitignore @@ -1,2 +1,2 @@ * -!.gitignore \ No newline at end of file +!.gitignore From 20b9b4221cfac811591ceacb184f87010109aa33 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 12:21:52 -0300 Subject: [PATCH 4/9] Apply fixed Coderabbitai review. --- .github/workflows/build.yml | 2 +- .github/workflows/dependency-check.yml | 2 +- .github/workflows/ecs.yml | 2 +- .github/workflows/static.yml | 2 +- .styleci.yml | 159 +++++++++++++------------ tests/TestSupportTest.php | 66 +++++----- 6 files changed, 117 insertions(+), 116 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3254d3d..9fef92b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,6 @@ permissions: jobs: phpunit: - uses: yii2-framework/actions/.github/workflows/phpunit.yml@main + uses: yii2-framework/actions/.github/workflows/phpunit.yml@v1 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index e69d7d1..294a347 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -18,4 +18,4 @@ permissions: jobs: composer-require-checker: - uses: yii2-framework/actions/.github/workflows/composer-require-checker.yml@main + uses: yii2-framework/actions/.github/workflows/composer-require-checker.yml@v1 diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml index e18f15c..76f6a37 100644 --- a/.github/workflows/ecs.yml +++ b/.github/workflows/ecs.yml @@ -18,4 +18,4 @@ permissions: jobs: easy-coding-standard: - uses: yii2-framework/actions/.github/workflows/ecs.yml@main + uses: yii2-framework/actions/.github/workflows/ecs.yml@v1 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 99d7c1c..cc1c99d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -18,4 +18,4 @@ permissions: jobs: phpstan: - uses: yii2-framework/actions/.github/workflows/phpstan.yml@main + uses: yii2-framework/actions/.github/workflows/phpstan.yml@v1 diff --git a/.styleci.yml b/.styleci.yml index 6245a5b..6cf29b2 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,86 +1,87 @@ preset: psr12 risky: true -version: 8 +version: 8.1 finder: - exclude: - - docs - - public - - vendor - - docker - - vargant + exclude: + - docs + - vendor enabled: - - alpha_ordered_traits - - array_indentation - - array_push - - combine_consecutive_issets - - combine_consecutive_unsets - - combine_nested_dirname - - declare_strict_types - - dir_constant - - fully_qualified_strict_types - - function_to_constant - - hash_to_slash_comment - - is_null - - logical_operators - - magic_constant_casing - - magic_method_casing - - method_separation - - modernize_types_casting - - native_function_casing - - native_function_type_declaration_casing - - no_alias_functions - - no_empty_comment - - no_empty_phpdoc - - no_empty_statement - - no_extra_block_blank_lines - - no_short_bool_cast - - no_superfluous_elseif - - no_unneeded_control_parentheses - - no_unneeded_curly_braces - - no_unneeded_final_method - - no_unset_cast - - no_unused_imports - - no_unused_lambda_imports - - no_useless_else - - no_useless_return - - normalize_index_brace - - php_unit_dedicate_assert - - php_unit_dedicate_assert_internal_type - - php_unit_expectation - - php_unit_mock - - php_unit_mock_short_will_return - - php_unit_namespaced - - php_unit_no_expectation_annotation - - phpdoc_no_empty_return - - phpdoc_no_useless_inheritdoc - - phpdoc_order - - phpdoc_property - - phpdoc_scalar - - phpdoc_separation - - phpdoc_singular_inheritdoc - - phpdoc_trim - - phpdoc_trim_consecutive_blank_line_separation - - phpdoc_type_to_var - - phpdoc_types - - phpdoc_types_order - - print_to_echo - - regular_callable_call - - return_assignment - - self_accessor - - self_static_accessor - - set_type_to_cast - - short_array_syntax - - short_list_syntax - - simplified_if_return - - single_quote - - standardize_not_equals - - ternary_to_null_coalescing - - trailing_comma_in_multiline_array - - unalign_double_arrow - - unalign_equals - - empty_loop_body_braces - - integer_literal_case - - union_type_without_spaces + - alpha_ordered_traits + - array_indentation + - array_push + - combine_consecutive_issets + - combine_consecutive_unsets + - combine_nested_dirname + - declare_strict_types + - dir_constant + - empty_loop_body_braces + - function_to_constant + - hash_to_slash_comment + - integer_literal_case + - is_null + - logical_operators + - magic_constant_casing + - magic_method_casing + - method_separation + - modernize_types_casting + - native_function_casing + - native_function_type_declaration_casing + - no_alias_functions + - no_empty_comment + - no_empty_phpdoc + - no_empty_statement + - no_extra_block_blank_lines + - no_short_bool_cast + - no_superfluous_elseif + - no_unneeded_control_parentheses + - no_unneeded_curly_braces + - no_unneeded_final_method + - no_unset_cast + - no_unused_imports + - no_unused_lambda_imports + - no_useless_else + - no_useless_return + - normalize_index_brace + - php_unit_dedicate_assert + - php_unit_dedicate_assert_internal_type + - php_unit_expectation + - php_unit_mock + - php_unit_mock_short_will_return + - php_unit_namespaced + - php_unit_no_expectation_annotation + - phpdoc_no_empty_return + - phpdoc_no_useless_inheritdoc + - phpdoc_order + - phpdoc_property + - phpdoc_scalar + - phpdoc_singular_inheritdoc + - phpdoc_trim + - phpdoc_trim_consecutive_blank_line_separation + - phpdoc_type_to_var + - phpdoc_types + - phpdoc_types_order + - print_to_echo + - regular_callable_call + - return_assignment + - self_accessor + - self_static_accessor + - set_type_to_cast + - short_array_syntax + - short_list_syntax + - simplified_if_return + - single_quote + - standardize_not_equals + - ternary_to_null_coalescing + - trailing_comma_in_multiline_array + - unalign_double_arrow + - unalign_equals + - union_type_without_spaces + +disabled: + - function_declaration + - new_with_parentheses + - psr12_braces + - psr12_class_definition diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php index fd8a534..9297689 100644 --- a/tests/TestSupportTest.php +++ b/tests/TestSupportTest.php @@ -70,39 +70,39 @@ public function testNormalizeLineEndingsWhenStringsAreIdenticalWithLineEndings() public function testRemoveFilesFromDirectoryRemovesAllFiles(): void { - $dir = __DIR__ . '/Support/runtime'; - - if (is_dir($dir)) { - unlink("{$dir}/.gitignore"); - unlink("{$dir}/.gitkeep"); - rmdir($dir); + $dir = sys_get_temp_dir() . '/php-forge-support-' . bin2hex(random_bytes(8)); + mkdir($dir); + + try { + mkdir("{$dir}/subdir"); + touch("{$dir}/test.txt"); + touch("{$dir}/subdir/test.txt"); + touch("{$dir}/.gitignore"); + touch("{$dir}/.gitkeep"); + + self::removeFilesFromDirectory($dir); + + self::assertFileDoesNotExist( + "{$dir}/test.txt", + "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.", + ); + self::assertFileDoesNotExist( + "{$dir}/subdir/test.txt", + "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.", + ); + self::assertFileExists( + "{$dir}/.gitignore", + "File '.gitignore' should remain after 'removeFilesFromDirectory' method is called.", + ); + self::assertFileExists( + "{$dir}/.gitkeep", + "File '.gitkeep' should remain after 'removeFilesFromDirectory' method is called.", + ); + } finally { + @unlink("{$dir}/.gitignore"); + @unlink("{$dir}/.gitkeep"); + @rmdir($dir); } - - mkdir($dir, 0777, true); - mkdir("{$dir}/subdir"); - touch("{$dir}/test.txt"); - touch("{$dir}/subdir/test.txt"); - touch("{$dir}/.gitignore"); - touch("{$dir}/.gitkeep"); - - self::removeFilesFromDirectory($dir); - - self::assertFileDoesNotExist( - "{$dir}/test.txt", - "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.", - ); - self::assertFileDoesNotExist( - "{$dir}/subdir/test.txt", - "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.", - ); - self::assertFileExists( - "{$dir}/.gitignore", - "File '.gitignore' should remain after 'removeFilesFromDirectory' method is called.", - ); - self::assertFileExists( - "{$dir}/.gitkeep", - "File '.gitkeep' should remain after 'removeFilesFromDirectory' method is called.", - ); } /** @@ -159,7 +159,7 @@ public function testSetInaccessibleParentProperty(): void self::assertSame( 'foo', self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'), - "Should return 'foo' after setting the parent property 'propertyParent' via " + "Should return 'value' when invoking the inaccessible method 'inaccessibleMethod' on 'TestClass' " . "'setInaccessibleParentProperty' method.", ); } From f5a9bd5f36f0e7dc54c7d1603343c00fff6da0f9 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 12:25:10 -0300 Subject: [PATCH 5/9] Apply fixed Coderabbitai review. --- .github/linters/actionlint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml index 785d850..328407a 100644 --- a/.github/linters/actionlint.yml +++ b/.github/linters/actionlint.yml @@ -4,4 +4,3 @@ paths: ignore: - '"pull_request" section is alias node but mapping node is expected' - '"push" section is alias node but mapping node is expected' - - "section is alias node but mapping node is expected" From 7a858173b55a2acbe0c23feab70ac2429cf65c6e Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 12:40:12 -0300 Subject: [PATCH 6/9] Apply fixed Coderabbitai review. --- tests/TestSupportTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php index 9297689..fbc6d11 100644 --- a/tests/TestSupportTest.php +++ b/tests/TestSupportTest.php @@ -125,7 +125,7 @@ public function testReturnValueWhenInvokingInaccessibleMethod(): void self::assertSame( 'value', self::invokeMethod(new TestClass(), 'inaccessibleMethod'), - "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " + "Should return 'value' when invoking the inaccessible method 'inaccessibleMethod' on 'TestClass' " . 'via reflection.', ); } @@ -159,8 +159,8 @@ public function testSetInaccessibleParentProperty(): void self::assertSame( 'foo', self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'), - "Should return 'value' when invoking the inaccessible method 'inaccessibleMethod' on 'TestClass' " - . "'setInaccessibleParentProperty' method.", + "Should return 'foo' after setting parent property 'propertyParent' " + . "via 'setInaccessibleParentProperty' method.", ); } From bea54c404b29c80d9aafc59863ac25696fcb8375 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 17:32:39 -0300 Subject: [PATCH 7/9] ffeat(support): Refactoring all code. --- README.md | 59 +++--- composer.json | 3 +- docs/svgs/features-mobile.svg | 4 +- docs/svgs/features.svg | 4 +- src/DirectoryCleaner.php | 76 +++++++ ...DataGenerator.php => EnumDataProvider.php} | 38 ++-- src/Exception/Message.php | 59 ++++++ src/LineEndingNormalizer.php | 43 ++++ src/{TestSupport.php => ReflectionHelper.php} | 99 ++------- tests/DirectoryCleanerTest.php | 71 +++++++ tests/EnumDataGeneratorTest.php | 150 -------------- tests/EnumDataProviderTest.php | 103 ++++++++++ tests/LineEndingNormalizerTest.php | 52 +++++ tests/ReflectionHelperTest.php | 121 +++++++++++ tests/Stub/TestBackedEnum.php | 19 ++ tests/Stub/TestEnum.php | 7 +- tests/Stub/TestIntBackedEnum.php | 19 ++ ...vider.php => EnumDataProviderProvider.php} | 27 ++- tests/Support/runtime/.gitignore | 0 tests/Support/runtime/.gitkeep | 0 tests/TestSupportTest.php | 190 ------------------ 21 files changed, 660 insertions(+), 484 deletions(-) create mode 100644 src/DirectoryCleaner.php rename src/{EnumDataGenerator.php => EnumDataProvider.php} (68%) create mode 100644 src/Exception/Message.php create mode 100644 src/LineEndingNormalizer.php rename src/{TestSupport.php => ReflectionHelper.php} (66%) create mode 100644 tests/DirectoryCleanerTest.php delete mode 100644 tests/EnumDataGeneratorTest.php create mode 100644 tests/EnumDataProviderTest.php create mode 100644 tests/LineEndingNormalizerTest.php create mode 100644 tests/ReflectionHelperTest.php create mode 100644 tests/Stub/TestBackedEnum.php create mode 100644 tests/Stub/TestIntBackedEnum.php rename tests/Support/Provider/{EnumDataGeneratorProvider.php => EnumDataProviderProvider.php} (61%) delete mode 100644 tests/Support/runtime/.gitignore delete mode 100644 tests/Support/runtime/.gitkeep delete mode 100644 tests/TestSupportTest.php diff --git a/README.md b/README.md index 1be003b..36b9036 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ composer require php-forge/support:^0.2 --dev ### Quick start -This package provides the `PHPForge\Support\TestSupport` trait for PHPUnit tests. +This package provides helper classes for PHPUnit tests. It supports reflection-based access to non-public members, deterministic string comparisons across platforms, and filesystem cleanup for isolated test environments. @@ -51,12 +51,11 @@ It supports reflection-based access to non-public members, deterministic string declare(strict_types=1); -use PHPForge\Support\TestSupport; +use PHPForge\Support\ReflectionHelper; use PHPUnit\Framework\TestCase; final class AccessPrivatePropertyTest extends TestCase { - use TestSupport; public function testInaccessibleProperty(): void { @@ -64,7 +63,7 @@ final class AccessPrivatePropertyTest extends TestCase private string $secretValue = 'hidden'; }; - $value = self::inaccessibleProperty($object, 'secretValue'); + $value = ReflectionHelper::inaccessibleProperty($object, 'secretValue'); self::assertSame('hidden', $value); } @@ -78,12 +77,11 @@ final class AccessPrivatePropertyTest extends TestCase declare(strict_types=1); -use PHPForge\Support\TestSupport; +use PHPForge\Support\ReflectionHelper; use PHPUnit\Framework\TestCase; final class InvokeProtectedMethodTest extends TestCase { - use TestSupport; public function testInvokeMethod(): void { @@ -94,7 +92,7 @@ final class InvokeProtectedMethodTest extends TestCase } }; - $result = self::invokeMethod($object, 'calculate', [5, 3]); + $result = ReflectionHelper::invokeMethod($object, 'calculate', [5, 3]); self::assertSame(8, $result); } @@ -108,18 +106,17 @@ final class InvokeProtectedMethodTest extends TestCase declare(strict_types=1); -use PHPForge\Support\TestSupport; +use PHPForge\Support\LineEndingNormalizer; use PHPUnit\Framework\TestCase; final class NormalizeLineEndingsTest extends TestCase { - use TestSupport; public function testNormalizedComparison(): void { self::assertSame( - self::normalizeLineEndings("Foo\r\nBar"), - self::normalizeLineEndings("Foo\nBar"), + LineEndingNormalizer::normalize("Foo\r\nBar"), + LineEndingNormalizer::normalize("Foo\nBar"), ); } } @@ -132,12 +129,11 @@ final class NormalizeLineEndingsTest extends TestCase declare(strict_types=1); -use PHPForge\Support\TestSupport; +use PHPForge\Support\DirectoryCleaner; use PHPUnit\Framework\TestCase; final class RemoveFilesFromDirectoryTest extends TestCase { - use TestSupport; public function testCleanup(): void { @@ -148,7 +144,7 @@ final class RemoveFilesFromDirectoryTest extends TestCase file_put_contents($testDir . '/artifact.txt', 'test'); file_put_contents($testDir . '/.gitignore', "*\n"); - self::removeFilesFromDirectory($testDir); + DirectoryCleaner::clean($testDir); self::assertFileDoesNotExist($testDir . '/artifact.txt'); self::assertFileExists($testDir . '/.gitignore'); @@ -167,12 +163,11 @@ final class RemoveFilesFromDirectoryTest extends TestCase declare(strict_types=1); -use PHPForge\Support\TestSupport; +use PHPForge\Support\ReflectionHelper; use PHPUnit\Framework\TestCase; final class SetInaccessiblePropertyTest extends TestCase { - use TestSupport; public function testSetProperty(): void { @@ -180,18 +175,18 @@ final class SetInaccessiblePropertyTest extends TestCase private string $config = 'default'; }; - self::setInaccessibleProperty($object, 'config', 'test-mode'); + ReflectionHelper::setInaccessibleProperty($object, 'config', 'test-mode'); - $newValue = self::inaccessibleProperty($object, 'config'); + $newValue = ReflectionHelper::inaccessibleProperty($object, 'config'); self::assertSame('test-mode', $newValue); } } ``` -#### Enum data generator +#### Enum data provider -Use `PHPForge\Support\EnumDataGenerator` to build deterministic datasets from `UnitEnum::cases()` and `UIAwesome\Html\Helper\Enum::normalizeValue()`. +Use `PHPForge\Support\EnumDataProvider` to build deterministic datasets from `UnitEnum::cases()` and normalized enum values. ##### Attribute fragment output (HTML) @@ -202,10 +197,9 @@ declare(strict_types=1); namespace App\Tests; -use PHPForge\Support\EnumDataGenerator; +use PHPForge\Support\EnumDataProvider; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use UIAwesome\Html\Helper\Enum; use UnitEnum; enum Size @@ -214,17 +208,17 @@ enum Size case Large; } -final class EnumDataGeneratorHtmlTest extends TestCase +final class EnumDataProviderHtmlTest extends TestCase { public static function provideEnumAttributes(): array { - return EnumDataGenerator::cases(Size::class, 'data-size', true); + return EnumDataProvider::attributeCases(Size::class, 'data-size', true); } #[DataProvider('provideEnumAttributes')] public function testBuildsAttributeFragment(UnitEnum $case, array $args, string $expected, string $message): void { - $normalized = (string) Enum::normalizeValue($case); + $normalized = $case->name; $attributeFragment = " data-size=\"{$normalized}\""; self::assertSame($expected, $attributeFragment, $message); @@ -241,7 +235,7 @@ declare(strict_types=1); namespace App\Tests; -use PHPForge\Support\EnumDataGenerator; +use PHPForge\Support\EnumDataProvider; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use UnitEnum; @@ -252,11 +246,11 @@ enum State case Disabled; } -final class EnumDataGeneratorEnumTest extends TestCase +final class EnumDataProviderEnumTest extends TestCase { public static function provideEnumInstances(): array { - return EnumDataGenerator::cases(State::class, 'state', false); + return EnumDataProvider::attributeCases(State::class, 'state', false); } #[DataProvider('provideEnumInstances')] @@ -276,10 +270,9 @@ declare(strict_types=1); namespace App\Tests; -use PHPForge\Support\EnumDataGenerator; +use PHPForge\Support\EnumDataProvider; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use UIAwesome\Html\Helper\Enum; enum Heading { @@ -287,17 +280,17 @@ enum Heading case H2; } -final class EnumDataGeneratorTagCasesTest extends TestCase +final class EnumDataProviderTagCasesTest extends TestCase { public static function provideTags(): array { - return EnumDataGenerator::tagCases(Heading::class, 'heading'); + return EnumDataProvider::tagCases(Heading::class, 'heading'); } #[DataProvider('provideTags')] public function testProvidesNormalizedTag(Heading $case, string $normalized): void { - self::assertSame((string) Enum::normalizeValue($case), $normalized); + self::assertSame($case->name, $normalized); } } ``` diff --git a/composer.json b/composer.json index 4602a9e..ab02bba 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": "^8.1", - "ui-awesome/html-helper": "^0.3" + "php": "^8.1" }, "require-dev": { "infection/infection": "^0.27|^0.32", diff --git a/docs/svgs/features-mobile.svg b/docs/svgs/features-mobile.svg index ff6dfe5..1ca7778 100644 --- a/docs/svgs/features-mobile.svg +++ b/docs/svgs/features-mobile.svg @@ -55,7 +55,7 @@

Enum dataset generation

-

Generate deterministic datasets for enum-based attribute test cases via EnumDataGenerator.

+

Generate deterministic datasets for enum-based attribute test cases via EnumDataProvider.

Filesystem test management

@@ -67,7 +67,7 @@

PHPUnit-focused helpers

-

Use the TestSupport trait to share test utilities across the suite.

+

Use helper classes to share test utilities across the suite.

diff --git a/docs/svgs/features.svg b/docs/svgs/features.svg index d971e80..b3a9610 100644 --- a/docs/svgs/features.svg +++ b/docs/svgs/features.svg @@ -52,7 +52,7 @@

Enum dataset generation

-

Generate deterministic datasets for enum-based attribute test cases via EnumDataGenerator.

+

Generate deterministic datasets for enum-based attribute test cases via EnumDataProvider.

Filesystem test management

@@ -64,7 +64,7 @@

PHPUnit-focused helpers

-

Use the TestSupport trait to share test utilities across the suite.

+

Use helper classes to share test utilities across the suite.

diff --git a/src/DirectoryCleaner.php b/src/DirectoryCleaner.php new file mode 100644 index 0000000..e2c17b5 --- /dev/null +++ b/src/DirectoryCleaner.php @@ -0,0 +1,76 @@ +getMessage($dirname), + ); + } + + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + + if ($file === '.gitignore' || $file === '.gitkeep') { + continue; + } + + $path = $basePath . DIRECTORY_SEPARATOR . $file; + + if (is_dir($path)) { + self::clean($path); + @rmdir($path); + } else { + @unlink($path); + } + } + + closedir($handle); + } +} diff --git a/src/EnumDataGenerator.php b/src/EnumDataProvider.php similarity index 68% rename from src/EnumDataGenerator.php rename to src/EnumDataProvider.php index aca3ff8..e7cb4d4 100644 --- a/src/EnumDataGenerator.php +++ b/src/EnumDataProvider.php @@ -4,23 +4,24 @@ namespace PHPForge\Support; -use UIAwesome\Html\Helper\Enum; +use BackedEnum; use UnitEnum; use function sprintf; /** - * Utility class for generating structured test data for enum-based attribute scenarios. + * Utility class for generating structured test data from enum cases. * - * Provides a standardized API for producing PHPUnit data sets built from `UnitEnum::cases()` and normalized values - * produced by {@see Enum::normalizeValue()}. + * Provides deterministic PHPUnit datasets built from `UnitEnum::cases()` and normalized enum values. * - * The generated data sets can be used to verify attribute fragments or enum instance handling in consumer tests. + * Key features. + * - Builds attribute fragment datasets via {@see EnumDataProvider::attributeCases()}. + * - Builds tag datasets via {@see EnumDataProvider::tagCases()}. * * @copyright Copyright (C) 2026 Terabytesoftw. * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License. */ -final class EnumDataGenerator +final class EnumDataProvider { /** * Generates test cases for enum-based attribute scenarios. @@ -32,19 +33,20 @@ final class EnumDataGenerator * * @param string $enumClass Enum class name implementing UnitEnum. * @param string|UnitEnum $attribute Attribute name used to build the expected fragment. - * @param bool $asHtml Whether to generate expected output as an attribute fragment or enum instance. Default is `true`. + * @param bool $asHtml Whether to generate expected output as an attribute fragment or enum instance. Default is + * `true`. * * @return array Structured test cases indexed by a normalized enum value key. * * @phpstan-return array */ - public static function cases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array + public static function attributeCases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array { $cases = []; - $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute)); + $attributeName = is_string($attribute) ? $attribute : sprintf('%s', self::normalizeValue($attribute)); foreach ($enumClass::cases() as $case) { - $normalizedValue = Enum::normalizeValue($case); + $normalizedValue = self::normalizeValue($case); $key = "enum: {$normalizedValue}"; $expected = $asHtml ? " {$attributeName}=\"{$normalizedValue}\"" : $case; @@ -66,25 +68,33 @@ public static function cases(string $enumClass, string|UnitEnum $attribute, bool /** * Generates test cases for tag-related enum scenarios. * - * Produces a dataset mapping descriptive keys to enum cases and their normalized string values, suitable for - * data provider methods in PHPUnit tests. + * Produces a dataset mapping descriptive keys to enum cases and their normalized string values, suitable for data + * provider methods in PHPUnit tests. * * @phpstan-param class-string $enumClass Enum class name implementing UnitEnum. * * @param string $enumClass Enum class name implementing UnitEnum. * @param string $category Category label appended to the generated keys. * - * @phpstan-return array Structured test cases indexed by descriptive keys. + * @phpstan-return array */ public static function tagCases(string $enumClass, string $category): array { $data = []; foreach ($enumClass::cases() as $case) { - $value = (string) Enum::normalizeValue($case); + $value = self::normalizeValue($case); $data[sprintf('%s %s tag', $value, $category)] = [$case, $value]; } return $data; } + + private static function normalizeValue(UnitEnum $enum): string + { + return match ($enum instanceof BackedEnum) { + true => (string) $enum->value, + false => $enum->name, + }; + } } diff --git a/src/Exception/Message.php b/src/Exception/Message.php new file mode 100644 index 0000000..77d239a --- /dev/null +++ b/src/Exception/Message.php @@ -0,0 +1,59 @@ +getMessage('/path/to/dir')); + * ``` + */ + public function getMessage(int|string ...$argument): string + { + return sprintf($this->value, ...$argument); + } +} diff --git a/src/LineEndingNormalizer.php b/src/LineEndingNormalizer.php new file mode 100644 index 0000000..587d48b --- /dev/null +++ b/src/LineEndingNormalizer.php @@ -0,0 +1,43 @@ + $args */ - private static function invokeMethod(object $object, string $method, array $args = []): mixed + public static function invokeMethod(object $object, string $method, array $args = []): mixed { $reflection = new ReflectionObject($object); @@ -128,7 +126,7 @@ private static function invokeMethod(object $object, string $method, array $args * @phpstan-param class-string $parentClass * @phpstan-param array $args */ - private static function invokeParentMethod( + public static function invokeParentMethod( object $object, string $parentClass, string $method, @@ -144,63 +142,6 @@ private static function invokeParentMethod( return $result ?? null; } - /** - * Normalizes line endings to `"\n"` for cross-platform string assertions. - * - * Converts `"\r\n"` and `"\r"` to `"\n"` to keep string comparisons deterministic across operating systems. - * - * @param string $line Input string potentially containing Windows style line endings. - * - * @return string String with normalized line endings. - */ - private static function normalizeLineEndings(string $line): string - { - return str_replace(["\r\n", "\r"], "\n", $line); - } - - /** - * Removes directory contents recursively while preserving `.gitignore` and `.gitkeep`. - * - * Iterates through `$basePath` and removes all files and subdirectories except `.`, `..`, `.gitignore`, and - * `.gitkeep`. Subdirectories are processed recursively before removal. - * - * File and directory removals are attempted with error suppression. - * - * @param string $basePath Absolute path to the directory whose contents will be removed. - * - * @throws RuntimeException if the directory cannot be opened for reading. - */ - private static function removeFilesFromDirectory(string $basePath): void - { - $handle = @opendir($basePath); - - if ($handle === false) { - $dirname = basename($basePath); - throw new RuntimeException("Unable to open directory: $dirname"); - } - - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - - if ($file === '.gitignore' || $file === '.gitkeep') { - continue; - } - - $path = $basePath . DIRECTORY_SEPARATOR . $file; - - if (is_dir($path)) { - self::removeFilesFromDirectory($path); - @rmdir($path); - } else { - @unlink($path); - } - } - - closedir($handle); - } - /** * Sets a parent class property value via reflection. * @@ -217,7 +158,7 @@ private static function removeFilesFromDirectory(string $basePath): void * * @phpstan-param class-string $parentClass */ - private static function setInaccessibleParentProperty( + public static function setInaccessibleParentProperty( object $object, string $parentClass, string $propertyName, @@ -246,7 +187,7 @@ private static function setInaccessibleParentProperty( * * @throws ReflectionException if the property does not exist. */ - private static function setInaccessibleProperty( + public static function setInaccessibleProperty( object $object, string $propertyName, mixed $value, diff --git a/tests/DirectoryCleanerTest.php b/tests/DirectoryCleanerTest.php new file mode 100644 index 0000000..ed3cfb5 --- /dev/null +++ b/tests/DirectoryCleanerTest.php @@ -0,0 +1,71 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('Unable to open directory: non-existing-directory'); + + DirectoryCleaner::clean(__DIR__ . '/non-existing-directory'); + } +} diff --git a/tests/EnumDataGeneratorTest.php b/tests/EnumDataGeneratorTest.php deleted file mode 100644 index c7d2a10..0000000 --- a/tests/EnumDataGeneratorTest.php +++ /dev/null @@ -1,150 +0,0 @@ - $enumClass - */ - #[DataProviderExternal(EnumDataGeneratorProvider::class, 'casesParameters')] - public function testCasesGenerateExpectedStructure( - string $enumClass, - string|UnitEnum $attribute, - bool $asHtml, - ): void { - $data = match ($asHtml) { - true => EnumDataGenerator::cases($enumClass, $attribute), - false => EnumDataGenerator::cases($enumClass, $attribute, false), - }; - - self::assertNotEmpty($data, 'Should return at least one data set.'); - - $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute)); - - foreach ($enumClass::cases() as $case) { - $expectedKey = 'enum: ' . Enum::normalizeValue($case); - - self::assertArrayHasKey( - $expectedKey, - $data, - "Should include a dataset key for enum case '{$case->name}'.", - ); - - $row = $data[$expectedKey] ?? null; - - self::assertIsArray( - $row, - "Should return a dataset array for enum case '{$case->name}'.", - ); - self::assertCount( - 4, - $row, - "Should return a 4-tuple dataset for enum case '{$case->name}'.", - ); - - /** @var array{UnitEnum, array, string|UnitEnum, string} $row */ - [$value, $attributes, $expected, $message] = $row; - - self::assertSame( - $case, - $value, - "Should store the enum case instance for '{$case->name}'.", - ); - self::assertSame( - [], - $attributes, - "Should store an empty attributes array for '{$case->name}'.", - ); - - $expectedValue = Enum::normalizeValue($case); - - if ($asHtml) { - self::assertSame( - " {$attributeName}=\"{$expectedValue}\"", - $expected, - "Should generate expected attribute fragment for '{$case->name}'.", - ); - $expectedMessage = "Should return the '{$attributeName}' attribute value for enum case: {$expectedValue}."; - } else { - self::assertSame( - $case, - $expected, - "Should return the enum instance as expected output for '{$case->name}'.", - ); - $expectedMessage = "Should return the enum instance for case: {$expectedValue}."; - } - - self::assertSame( - $expectedMessage, - $message, - "Should store the expected message for '{$case->name}'.", - ); - } - } - - /** - * @phpstan-param class-string $enumClass - */ - #[DataProviderExternal(EnumDataGeneratorProvider::class, 'tagCasesParameters')] - public function testTagCasesGenerateExpectedStructure(string $enumClass, string $category): void - { - $data = EnumDataGenerator::tagCases($enumClass, $category); - - self::assertNotEmpty($data, 'Should return at least one data set.'); - - foreach ($enumClass::cases() as $case) { - $expectedKey = Enum::normalizeValue($case) . " {$category} tag"; - - self::assertArrayHasKey( - $expectedKey, - $data, - "Should include a dataset key for enum case '{$case->name}'.", - ); - - $row = $data[$expectedKey] ?? null; - - self::assertIsArray( - $row, - "Should return a dataset array for enum case '{$case->name}'.", - ); - self::assertCount( - 2, - $row, - "Should return a 2-tuple dataset for enum case '{$case->name}'.", - ); - - /** @var array{UnitEnum, string} $row */ - [$value, $normalizedValue] = $row; - - self::assertSame( - $case, - $value, - "Should store the enum case instance for '{$case->name}'.", - ); - self::assertSame( - Enum::normalizeValue($case), - $normalizedValue, - "Should store normalized value for '{$case->name}'.", - ); - } - } -} diff --git a/tests/EnumDataProviderTest.php b/tests/EnumDataProviderTest.php new file mode 100644 index 0000000..ff8fb95 --- /dev/null +++ b/tests/EnumDataProviderTest.php @@ -0,0 +1,103 @@ + $enumClass + */ + #[DataProviderExternal(EnumDataProviderProvider::class, 'casesParameters')] + public function testCasesGenerateExpectedStructure( + string $enumClass, + string|UnitEnum $attribute, + bool $asHtml, + string $expectedKeyCase, + string $expectedAttributeCase, + string $expectedMessage, + ): void { + $data = match ($asHtml) { + true => EnumDataProvider::attributeCases($enumClass, $attribute), + false => EnumDataProvider::attributeCases($enumClass, $attribute, false), + }; + + self::assertNotEmpty( + $data, + 'Should return at least one data set.', + ); + self::assertArrayHasKey( + $expectedKeyCase, + $data, + 'Should contain expected enum case in dataset.', + ); + self::assertInstanceOf( + $enumClass, + $data[$expectedKeyCase][0] ?? null, + 'Should return expected enum case instance.', + ); + + if ($asHtml) { + self::assertSame( + $expectedAttributeCase, + $data[$expectedKeyCase][2], + 'Should return expected attribute value for enum case.', + ); + } + + self::assertSame( + $expectedMessage, + $data[$expectedKeyCase][3], + 'Should return expected message for enum case.', + ); + } + + public function testTagCasesGenerateExpectedStructure(): void + { + $data = EnumDataProvider::tagCases(TestEnum::class, 'element'); + + self::assertNotEmpty( + $data, + 'Should return at least one data set.', + ); + self::assertSame( + [ + 'BAR element tag' => [ + TestEnum::BAR, + 'BAR', + ], + 'FOO element tag' => [ + TestEnum::FOO, + 'FOO', + ], + ], + $data, + 'Should return expected tag cases dataset.', + ); + } +} diff --git a/tests/LineEndingNormalizerTest.php b/tests/LineEndingNormalizerTest.php new file mode 100644 index 0000000..f44ffad --- /dev/null +++ b/tests/LineEndingNormalizerTest.php @@ -0,0 +1,52 @@ + + * @return array */ public static function casesParameters(): array { return [ - 'as html' => [ + 'as enum instance' => [ TestEnum::class, 'data-test', - true, + false, + 'enum: FOO', + ' data-test="FOO"', + 'Should return the enum instance for case: FOO.', ], - 'as enum instance' => [ + 'as html' => [ TestEnum::class, 'data-test', - false, + true, + 'enum: BAR', + ' data-test="BAR"', + "Should return the 'data-test' attribute value for enum case: BAR.", ], 'attribute as enum instance' => [ TestEnum::class, - TestEnum::Foo, + TestEnum::FOO, true, + 'enum: FOO', + ' FOO="FOO"', + "Should return the 'FOO' attribute value for enum case: FOO.", ], ]; } diff --git a/tests/Support/runtime/.gitignore b/tests/Support/runtime/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Support/runtime/.gitkeep b/tests/Support/runtime/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php deleted file mode 100644 index fbc6d11..0000000 --- a/tests/TestSupportTest.php +++ /dev/null @@ -1,190 +0,0 @@ -expectException(RuntimeException::class); - $this->expectExceptionMessage('Unable to open directory: non-existing-directory'); - - self::removeFilesFromDirectory(__DIR__ . '/non-existing-directory'); - } -} From 30bc1e38c2e19e007f636dcaaf43c141c9ef40c9 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 19 Jan 2026 20:32:59 +0000 Subject: [PATCH 8/9] Apply fixes from StyleCI --- tests/LineEndingNormalizerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/LineEndingNormalizerTest.php b/tests/LineEndingNormalizerTest.php index f44ffad..e0aacce 100644 --- a/tests/LineEndingNormalizerTest.php +++ b/tests/LineEndingNormalizerTest.php @@ -32,6 +32,7 @@ public function testNormalizeConvertsLiteralBackslashNToNewline(): void "Should normalize literal '\\n' sequences to 'LF'.", ); } + public function testNormalizeReturnsUnchangedStringWhenNoNormalizationNeeded(): void { self::assertSame( From 8c7d22f22bf6e6565413910614bde0438ed46039 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 19 Jan 2026 17:51:16 -0300 Subject: [PATCH 9/9] Apply fixed Coderabbitai review. --- src/DirectoryCleaner.php | 3 ++- src/ReflectionHelper.php | 2 -- tests/ReflectionHelperTest.php | 18 ++++++++++++++++++ .../Provider/EnumDataProviderProvider.php | 13 ------------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/DirectoryCleaner.php b/src/DirectoryCleaner.php index e2c17b5..7bcc327 100644 --- a/src/DirectoryCleaner.php +++ b/src/DirectoryCleaner.php @@ -10,6 +10,7 @@ use function basename; use function closedir; use function is_dir; +use function is_link; use function opendir; use function readdir; use function rmdir; @@ -63,7 +64,7 @@ public static function clean(string $basePath): void $path = $basePath . DIRECTORY_SEPARATOR . $file; - if (is_dir($path)) { + if (is_dir($path) && is_link($path) === false) { self::clean($path); @rmdir($path); } else { diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php index 870c505..96f326e 100644 --- a/src/ReflectionHelper.php +++ b/src/ReflectionHelper.php @@ -198,7 +198,5 @@ public static function setInaccessibleProperty( $property = $class->getProperty($propertyName); $property->setValue($object, $value); } - - unset($class, $property); } } diff --git a/tests/ReflectionHelperTest.php b/tests/ReflectionHelperTest.php index 5661182..fd46888 100644 --- a/tests/ReflectionHelperTest.php +++ b/tests/ReflectionHelperTest.php @@ -44,6 +44,14 @@ public function testInaccessibleParentPropertyReturnsExpectedValue(): void ); } + public function testInaccessiblePropertyReturnsNullForEmptyPropertyName(): void + { + self::assertNull( + ReflectionHelper::inaccessibleProperty(new TestClass(), ''), + 'Should return null when property name is empty.', + ); + } + /** * @throws ReflectionException */ @@ -118,4 +126,14 @@ public function testSetInaccessiblePropertySetsValueCorrectly(): void "Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.", ); } + + public function testThrowReflectionExceptionForNonExistentProperty(): void + { + $this->expectException(ReflectionException::class); + $this->expectExceptionMessage( + 'Property PHPForge\Support\Tests\Stub\TestClass::$nonExistent does not exist', + ); + + ReflectionHelper::inaccessibleProperty(new TestClass(), 'nonExistent'); + } } diff --git a/tests/Support/Provider/EnumDataProviderProvider.php b/tests/Support/Provider/EnumDataProviderProvider.php index a3a66b7..4a0a47f 100644 --- a/tests/Support/Provider/EnumDataProviderProvider.php +++ b/tests/Support/Provider/EnumDataProviderProvider.php @@ -48,17 +48,4 @@ public static function casesParameters(): array ], ]; } - - /** - * @return array - */ - public static function tagCasesParameters(): array - { - return [ - 'default category' => [ - TestEnum::class, - 'element', - ], - ]; - } }