From 985a7628020d84d197a876d20ac46f143b073531 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Sun, 25 Feb 2024 13:05:02 +1100 Subject: [PATCH] Update to scaffold 0.12.1 (#57) --- .editorconfig | 14 +- .gitattributes | 17 ++- .github/PULL_REQUEST_TEMPLATE.md | 9 +- .github/release-drafter.yml | 8 +- ...assign-pr-author.yml => assign-author.yml} | 4 +- .../{release.yml => draft-release-notes.yml} | 7 +- .gitignore | 11 +- README.md | 2 +- composer.json | 28 ++-- phpstan.neon | 7 + phpunit.xml | 6 +- rector.php | 65 +++++++++ src/ArtefactTrait.php | 25 ++-- src/FilesystemTrait.php | 12 +- src/GitTrait.php | 19 +-- src/TokenTrait.php | 2 +- tests/AbstractTestCase.php | 136 +----------------- .../AbstractFunctionalTestCase.php} | 6 +- .../BranchTest.php | 6 +- .../ForcePushTest.php | 4 +- .../GeneralTest.php | 4 +- tests/{Integration => Functional}/README.md | 0 tests/{Integration => Functional}/TagTest.php | 6 +- .../{Integration => Functional}/TokenTest.php | 14 +- tests/{ => Traits}/CommandTrait.php | 50 +++---- tests/Traits/MockTrait.php | 77 ++++++++++ tests/Traits/ReflectionTrait.php | 103 +++++++++++++ tests/Unit/AbstractUnitTestCase.php | 3 +- tests/Unit/ExcludeTest.php | 1 - 29 files changed, 397 insertions(+), 249 deletions(-) rename .github/workflows/{auto-assign-pr-author.yml => assign-author.yml} (71%) rename .github/workflows/{release.yml => draft-release-notes.yml} (72%) create mode 100644 rector.php rename tests/{Integration/AbstractIntegrationTestCase.php => Functional/AbstractFunctionalTestCase.php} (97%) rename tests/{Integration => Functional}/BranchTest.php (97%) rename tests/{Integration => Functional}/ForcePushTest.php (99%) rename tests/{Integration => Functional}/GeneralTest.php (97%) rename tests/{Integration => Functional}/README.md (100%) rename tests/{Integration => Functional}/TagTest.php (88%) rename tests/{Integration => Functional}/TokenTest.php (88%) rename tests/{ => Traits}/CommandTrait.php (93%) create mode 100644 tests/Traits/MockTrait.php create mode 100644 tests/Traits/ReflectionTrait.php diff --git a/.editorconfig b/.editorconfig index c27ebe5..4a2cc0e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,3 @@ -# Drupal editor configuration normalization -# @see http://editorconfig.org/ - # This is the top-most .editorconfig file; do not search in parent directories. root = true @@ -13,10 +10,17 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[composer.{json,lock}] +[*.php] +indent_size = 2 + + +[*.{json,lock}] indent_size = 4 -[*.yml] +[*.{yml,yaml}] +indent_size = 2 + +[*.{sh,bash,bats}] indent_size = 2 [*.xml] diff --git a/.gitattributes b/.gitattributes index a77a194..7ec53ed 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,12 @@ # Ignore files for distribution archives. -.gitattributes export-ignore -.gitignore export-ignore -.editorconfig export-ignore -tests export-ignore -.circle export-ignore -phpcs.xml export-ignore -phpunit.xml export-ignore +/.circleci export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/tests export-ignore +/phpcs.xml export-ignore +/phpmd.xml export-ignore +/phpstan.neon export-ignore +/phpunit.xml export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7c34e35..95cbae2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,12 @@ ## Checklist before requesting a review -- [ ] I have formatted the subject to include ticket number - as `[#123] Verb in past tense with dot at the end.` +- [ ] I have formatted the subject to include ticket number as `[#123] Verb in past tense with dot at the end.` - [ ] I have added a link to the issue tracker -- [ ] I have provided information in `Changed` section about WHY something was - done if this was not a normal implementation +- [ ] I have provided information in `Changed` section about WHY something was done if this was not a normal implementation - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] I have run new and existing relevant tests locally with my changes, and - they passed +- [ ] I have run new and existing relevant tests locally with my changes, and they passed - [ ] I have provided screenshots, where applicable ## Changed diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 699ff47..8b07383 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,12 +1,14 @@ -name-template: '$NEXT_MINOR_VERSION' -tag-template: '$NEXT_MINOR_VERSION' +name-template: '$RESOLVED_VERSION' +tag-template: '$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + default: minor template: | ## What's new since $PREVIOUS_TAG $CHANGES - **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$NEXT_MINOR_VERSION + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$RESOLVED_VERSION $CONTRIBUTORS diff --git a/.github/workflows/auto-assign-pr-author.yml b/.github/workflows/assign-author.yml similarity index 71% rename from .github/workflows/auto-assign-pr-author.yml rename to .github/workflows/assign-author.yml index df73ac4..466655f 100644 --- a/.github/workflows/auto-assign-pr-author.yml +++ b/.github/workflows/assign-author.yml @@ -12,5 +12,7 @@ permissions: jobs: assign-author: runs-on: ubuntu-latest + steps: - - uses: toshimaru/auto-author-assign@v2.1.0 + - name: Assign author + uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/draft-release-notes.yml similarity index 72% rename from .github/workflows/release.yml rename to .github/workflows/draft-release-notes.yml index c6587e7..c9e0353 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/draft-release-notes.yml @@ -1,4 +1,4 @@ -name: Release +name: Draft release notes on: push: @@ -15,8 +15,11 @@ jobs: permissions: contents: write pull-requests: write + runs-on: ubuntu-latest + steps: - - uses: release-drafter/release-drafter@v6 + - name: Draft release notes + uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 5690759..522ecdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ # To ignore OS temporary files use global .gitignore # https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer -vendor -artifact -composer.lock -git_* -.phpunit.cache +/.build +/.coverage-html +/.phpunit.cache +/cobertura.xml +/composer.lock +/vendor diff --git a/README.md b/README.md index 7e24b23..3596a15 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ would reult in the branch name `release/1.2.0-secondtag`. ```bash composer lint -composer lint:fix +composer lint-fix ``` ### Run tests diff --git a/composer.json b/composer.json index dd9730c..3ffddba 100644 --- a/composer.json +++ b/composer.json @@ -24,17 +24,9 @@ "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.5", + "rector/rector": "^1.0.0", "squizlabs/php_codesniffer": "^3.6" }, - "scripts": { - "lint": [ - "phpcs", - "phpmd --exclude vendor . text phpmd.xml", - "phpstan" - ], - "lint:fix": "phpcbf", - "test": "if [ \"${XDEBUG_MODE}\" = 'coverage' ]; then phpunit; else phpunit --no-coverage; fi" - }, "autoload": { "psr-4": { "DrevOps\\Robo\\": "src" @@ -49,5 +41,23 @@ "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } + }, + "scripts": { + "lint": [ + "phpcs", + "phpmd --exclude vendor,vendor-bin,node_modules . text phpmd.xml", + "phpstan", + "rector --clear-cache --dry-run" + ], + "lint-fix": [ + "rector --clear-cache", + "phpcbf" + ], + "test": "if [ \"${XDEBUG_MODE}\" = 'coverage' ]; then phpunit; else phpunit --no-coverage; fi", + "build": [ + "@composer bin box require --dev humbug/box", + "box validate", + "box compile" + ] } } diff --git a/phpstan.neon b/phpstan.neon index 634fb22..351833b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,3 +16,10 @@ parameters: - node_modules/* ignoreErrors: + - + # Since tests and data providers do not have to have parameter docblocks, + # it is not possible to specify the type of the parameter, so we ignore + # this error. + message: '#.*no value type specified in iterable type array.#' + path: tests/* + reportUnmatched: false diff --git a/phpunit.xml b/phpunit.xml index ac2fb05..d8fcc3f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,14 +14,14 @@ displayDetailsOnTestsThatTriggerNotices="true" > - - ./tests/ + + tests - src + src diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..a158b55 --- /dev/null +++ b/rector.php @@ -0,0 +1,65 @@ +paths([ + __DIR__ . '/**', + ]); + + $rectorConfig->sets([ + SetList::PHP_80, + SetList::PHP_81, + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::DEAD_CODE, + SetList::INSTANCEOF, + SetList::TYPE_DECLARATION, + ]); + + $rectorConfig->skip([ + // Rules added by Rector's rule sets. + ArraySpreadInsteadOfArrayMergeRector::class, + CountArrayToEmptyArrayComparisonRector::class, + DisallowedEmptyRuleFixerRector::class, + InlineArrayReturnAssignRector::class, + NewlineAfterStatementRector::class, + NewlineBeforeNewAssignSetRector::class, + PostIncDecToPreIncDecRector::class, + RemoveAlwaysTrueIfConditionRector::class, + SimplifyEmptyCheckOnEmptyArrayRector::class, + // Dependencies. + '*/vendor/*', + '*/node_modules/*', + ]); + + $rectorConfig->fileExtensions([ + 'php', + 'inc', + ]); + + $rectorConfig->importNames(TRUE, FALSE); + $rectorConfig->importShortClasses(FALSE); +}; diff --git a/src/ArtefactTrait.php b/src/ArtefactTrait.php index afe4d09..b4f98e3 100644 --- a/src/ArtefactTrait.php +++ b/src/ArtefactTrait.php @@ -38,7 +38,7 @@ trait ArtefactTrait * * @var string|null */ - protected $originalBranch = null; + protected $originalBranch; /** * Destination branch with optional tokens. @@ -277,7 +277,7 @@ protected function doPush(): void $this->result = $result->wasSuccessful(); } catch (\Exception $exception) { // Re-throw the message with additional context. - throw new \Exception(sprintf('Error occurred while pushing branch "%s": %s', $this->dstBranch, $exception->getMessage())); + throw new \Exception(sprintf('Error occurred while pushing branch "%s": %s', $this->dstBranch, $exception->getMessage()), $exception->getCode(), $exception); } if ($this->result) { @@ -301,7 +301,7 @@ protected function doPush(): void */ protected function resolveOptions(array $options): void { - $this->now = !empty($options['now']) ? (int) $options['now'] : time(); + $this->now = empty($options['now']) ? time() : (int) $options['now']; $this->debug = !empty($options['debug']); @@ -310,7 +310,7 @@ protected function resolveOptions(array $options): void $this->fsSetRootDir($options['root']); // Default source to the root directory. - $srcPath = !empty($options['src']) ? $this->fsGetAbsolutePath($options['src']) : $this->fsGetRootDir(); + $srcPath = empty($options['src']) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($options['src']); $this->gitSetSrcRepo($srcPath); $this->originalBranch = $this->resolveOriginalBranch($this->src); @@ -329,7 +329,7 @@ protected function resolveOptions(array $options): void $this->needsPush = !empty($options['push']); - $this->report = !empty($options['report']) ? $options['report'] : null; + $this->report = empty($options['report']) ? null : $options['report']; $this->setMode($options['mode'], $options); } @@ -617,11 +617,11 @@ protected function localExcludeEmpty(string $path, bool $strict = false): bool $lines = file($filename); if ($lines) { $lines = array_map('trim', $lines); - $lines = array_filter($lines, function ($line) { + $lines = array_filter($lines, static function ($line) : bool { return strlen($line) > 0; }); - $lines = array_filter($lines, function ($line) { - return strpos(trim($line), '#') !== 0; + $lines = array_filter($lines, static function ($line) : bool { + return !str_starts_with(trim($line), '#'); }); } @@ -856,20 +856,21 @@ protected function sayOkay(string $text): void { $color = 'green'; $char = $this->decorationCharacter('V', '✔'); - $format = "%s %s"; + $format = sprintf('%%s %%s', $color, $color); $this->writeln(sprintf($format, $char, $text)); } /** * Print debug information. + * + * @param mixed ...$args + * The args. */ - protected function printDebug(): void + protected function printDebug(mixed ...$args): void { if (!$this->debug) { return; } - - $args = func_get_args(); $message = array_shift($args); /* @phpstan-ignore-next-line */ $this->writeln(vsprintf($message, $args)); diff --git a/src/FilesystemTrait.php b/src/FilesystemTrait.php index 2a862ae..42f493e 100644 --- a/src/FilesystemTrait.php +++ b/src/FilesystemTrait.php @@ -60,7 +60,7 @@ public function __construct() */ protected function fsSetRootDir(string $path = null): void { - $path = !empty($path) ? $this->fsGetAbsolutePath($path) : $this->fsGetRootDir(); + $path = empty($path) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($path); $this->fsPathsExist($path); $this->fsRootDir = $path; } @@ -219,10 +219,10 @@ protected function fsPathsExist($paths, bool $strict = true): bool protected function realpath(string $path): string { // Whether $path is unix or not. - $unipath = strlen($path) === 0 || $path[0] !== '/'; - $unc = substr($path, 0, 2) === '\\\\' ? true : false; + $unipath = $path === '' || $path[0] !== '/'; + $unc = str_starts_with($path, '\\\\'); // Attempt to detect if path is relative in which case, add cwd. - if (strpos($path, ':') === false && $unipath && !$unc) { + if (!str_contains($path, ':') && $unipath && !$unc) { $path = getcwd().DIRECTORY_SEPARATOR.$path; if ($path[0] === '/') { $unipath = false; @@ -231,7 +231,7 @@ protected function realpath(string $path): string // Resolve path parts (single dot, double dot and double delimiters). $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); - $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), function ($part) { + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), static function ($part) : bool { return strlen($part) > 0; }); @@ -252,7 +252,7 @@ protected function realpath(string $path): string $path = readlink($path); } // Put initial separator that could have been lost. - $path = !$unipath ? '/'.$path : $path; + $path = $unipath ? $path : '/'.$path; /* @phpstan-ignore-next-line */ return $unc ? '\\\\'.$path : $path; diff --git a/src/GitTrait.php b/src/GitTrait.php index f2979e2..b594729 100644 --- a/src/GitTrait.php +++ b/src/GitTrait.php @@ -381,19 +381,12 @@ protected function gitIsRemote(string $location, string $type = 'any'): bool $isLocal = $this->fsPathsExist($this->fsGetAbsolutePath($location), false); $isUri = self::gitIsUri($location); - switch ($type) { - case 'any': - return $isLocal || $isUri; - - case 'local': - return $isLocal; - - case 'uri': - return $isUri; - - default: - throw new \InvalidArgumentException(sprintf('Invalid argument "%s" provided', $type)); - } + return match ($type) { + 'any' => $isLocal || $isUri, + 'local' => $isLocal, + 'uri' => $isUri, + default => throw new \InvalidArgumentException(sprintf('Invalid argument "%s" provided', $type)), + }; } /** diff --git a/src/TokenTrait.php b/src/TokenTrait.php index ca57ef3..88424d7 100644 --- a/src/TokenTrait.php +++ b/src/TokenTrait.php @@ -22,7 +22,7 @@ trait TokenTrait */ protected function tokenProcess(string $string): ?string { - return preg_replace_callback('/(?:\[([^\]]+)\])/', function ($match) { + return preg_replace_callback('/(?:\[([^\]]+)\])/', function (array $match) { if (count($match) > 1) { $parts = explode(':', $match[1], 2); $token = isset($parts[0]) ? $parts[0] : null; diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 9bf11e1..f14403e 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -4,6 +4,9 @@ namespace DrevOps\Robo\Tests; +use DrevOps\Robo\Tests\Traits\CommandTrait; +use DrevOps\Robo\Tests\Traits\MockTrait; +use DrevOps\Robo\Tests\Traits\ReflectionTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; @@ -21,6 +24,9 @@ abstract class AbstractTestCase extends TestCase CommandTrait::runRoboCommand as public commandRunRoboCommand; } + use ReflectionTrait; + use MockTrait; + /** * File system. * @@ -66,136 +72,6 @@ protected function tearDown(): void } } - /** - * Call protected methods on the class. - * - * @param object|class-string $object - * Object or class name to use for a method call. - * @param string $method - * Method name. Method can be static. - * @param array $args - * Array of arguments to pass to the method. To pass arguments by - * reference, pass them by reference as an element of this array. - * - * @return mixed - * Method result. - * - * @throws \ReflectionException - */ - protected static function callProtectedMethod($object, string $method, array $args = []) - { - $class = new \ReflectionClass(is_object($object) ? get_class($object) : $object); - $method = $class->getMethod($method); - $method->setAccessible(true); - $object = $method->isStatic() ? null : $object; - - /* @phpstan-ignore-next-line */ - return $method->invokeArgs($object, $args); - } - - /** - * Set protected property value. - * - * @param object $object - * Object to set the value on. - * @param string $property - * Property name to set the value. Property should exists in the object. - * @param mixed $value - * Value to set to the property. - * - * @throws \ReflectionException - */ - protected static function setProtectedValue($object, string $property, $value): void - { - $class = new \ReflectionClass(get_class($object)); - $property = $class->getProperty($property); - $property->setAccessible(true); - - $property->setValue($object, $value); - } - - /** - * Get protected value from the object. - * - * @param object $object - * Object to set the value on. - * @param string $property - * Property name to get the value. Property should exists in the object. - * - * @return mixed - * Protected property value. - */ - protected static function getProtectedValue($object, $property) - { - $class = new \ReflectionClass(get_class($object)); - $property = $class->getProperty($property); - $property->setAccessible(true); - - return $property->getValue($class); - } - - /** - * Helper to prepare class or trait mock. - * - * @param class-string $class - * Class or trait name to generate the mock. - * @param array $methodsMap - * Optional array of methods and values, keyed by method name. Array - * elements can be return values, callbacks created with - * $this->returnCallback(), or closures. - * @param array $args - * Optional array of constructor arguments. If omitted, a constructor - * will not be called. - * - * @return object - * Mocked class. - * - * @throws \ReflectionException - */ - protected function prepareMock($class, array $methodsMap = [], array $args = []) - { - $methods = array_keys($methodsMap); - - $reflectionClass = new \ReflectionClass($class); - - if ($reflectionClass->isAbstract()) { - $mock = $this->getMockForAbstractClass($class, $args, '', !empty($args), true, true, $methods); - } elseif ($reflectionClass->isTrait()) { - $mock = $this->getMockForTrait($class, [], '', true, true, true, array_keys($methodsMap)); - } else { - $mockBuilder = $this->getMockBuilder($class); - if (!empty($args)) { - $mockBuilder = $mockBuilder->enableOriginalConstructor() - ->setConstructorArgs($args); - } else { - $mockBuilder = $mockBuilder->disableOriginalConstructor(); - } - /* @todo setMethods method is not found on MockBuilder */ - /* @phpstan-ignore-next-line */ - $mock = $mockBuilder->setMethods($methods) - ->getMock(); - } - - foreach ($methodsMap as $method => $value) { - // Handle callback values differently. - if (is_object($value) && strpos(get_class($value), 'Callback') !== false) { - $mock->expects($this->any()) - ->method($method) - ->will($value); - } elseif (is_object($value) && strpos(get_class($value), 'Closure') !== false) { - $mock->expects($this->any()) - ->method($method) - ->will($this->returnCallback($value)); - } else { - $mock->expects($this->any()) - ->method($method) - ->willReturn($value); - } - } - - return $mock; - } - /** * Check if testing framework was ran with --debug option. * diff --git a/tests/Integration/AbstractIntegrationTestCase.php b/tests/Functional/AbstractFunctionalTestCase.php similarity index 97% rename from tests/Integration/AbstractIntegrationTestCase.php rename to tests/Functional/AbstractFunctionalTestCase.php index 8cc4acb..8c6cd12 100644 --- a/tests/Integration/AbstractIntegrationTestCase.php +++ b/tests/Functional/AbstractFunctionalTestCase.php @@ -2,14 +2,14 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Integration; +namespace DrevOps\Robo\Tests\Functional; use DrevOps\Robo\Tests\AbstractTestCase; /** * Class AbstractTestCase */ -abstract class AbstractIntegrationTestCase extends AbstractTestCase +abstract class AbstractFunctionalTestCase extends AbstractTestCase { /** @@ -154,7 +154,7 @@ protected function runBuild(string $args = '', bool $expectFail = false): string protected function runRoboCommandTimestamped(string $command, bool $expectFail = false): array { // Add --now option to all 'artifact' commands. - if (strpos($command, 'artifact') === 0) { + if (str_starts_with($command, 'artifact')) { $command .= ' --now='.$this->now; } diff --git a/tests/Integration/BranchTest.php b/tests/Functional/BranchTest.php similarity index 97% rename from tests/Integration/BranchTest.php rename to tests/Functional/BranchTest.php index 56399db..b2846fc 100644 --- a/tests/Integration/BranchTest.php +++ b/tests/Functional/BranchTest.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Integration; +namespace DrevOps\Robo\Tests\Functional; /** * Class BranchTest. @@ -13,13 +13,13 @@ * @covers \DrevOps\Robo\ArtefactTrait * @covers \DrevOps\Robo\FilesystemTrait */ -class BranchTest extends AbstractIntegrationTestCase +class BranchTest extends AbstractFunctionalTestCase { /** * {@inheritdoc} */ - public function setUp(): void + protected function setUp(): void { $this->mode = 'branch'; parent::setUp(); diff --git a/tests/Integration/ForcePushTest.php b/tests/Functional/ForcePushTest.php similarity index 99% rename from tests/Integration/ForcePushTest.php rename to tests/Functional/ForcePushTest.php index 1d6fc2d..73bc479 100644 --- a/tests/Integration/ForcePushTest.php +++ b/tests/Functional/ForcePushTest.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Integration; +namespace DrevOps\Robo\Tests\Functional; /** * Class ForcePushTest. @@ -15,7 +15,7 @@ * @covers \DrevOps\Robo\ArtefactTrait * @covers \DrevOps\Robo\FilesystemTrait */ -class ForcePushTest extends AbstractIntegrationTestCase +class ForcePushTest extends AbstractFunctionalTestCase { protected function setUp(): void diff --git a/tests/Integration/GeneralTest.php b/tests/Functional/GeneralTest.php similarity index 97% rename from tests/Integration/GeneralTest.php rename to tests/Functional/GeneralTest.php index 3e1a6ca..720ecd5 100644 --- a/tests/Integration/GeneralTest.php +++ b/tests/Functional/GeneralTest.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Integration; +namespace DrevOps\Robo\Tests\Functional; /** * Class GeneralTest. @@ -12,7 +12,7 @@ * @covers \DrevOps\Robo\ArtefactTrait * @covers \DrevOps\Robo\FilesystemTrait */ -class GeneralTest extends AbstractIntegrationTestCase +class GeneralTest extends AbstractFunctionalTestCase { public function testPresence(): void diff --git a/tests/Integration/README.md b/tests/Functional/README.md similarity index 100% rename from tests/Integration/README.md rename to tests/Functional/README.md diff --git a/tests/Integration/TagTest.php b/tests/Functional/TagTest.php similarity index 88% rename from tests/Integration/TagTest.php rename to tests/Functional/TagTest.php index b0e4bdd..cc780c3 100644 --- a/tests/Integration/TagTest.php +++ b/tests/Functional/TagTest.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Integration; +namespace DrevOps\Robo\Tests\Functional; /** * Class TagTest. @@ -13,13 +13,13 @@ * @covers \DrevOps\Robo\ArtefactTrait * @covers \DrevOps\Robo\FilesystemTrait */ -class TagTest extends AbstractIntegrationTestCase +class TagTest extends AbstractFunctionalTestCase { /** * {@inheritdoc} */ - public function setUp(): void + protected function setUp(): void { $this->mode = 'force-push'; parent::setUp(); diff --git a/tests/Integration/TokenTest.php b/tests/Functional/TokenTest.php similarity index 88% rename from tests/Integration/TokenTest.php rename to tests/Functional/TokenTest.php index 25e1176..7268426 100644 --- a/tests/Integration/TokenTest.php +++ b/tests/Functional/TokenTest.php @@ -2,7 +2,9 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Integration; +namespace DrevOps\Robo\Tests\Functional; + +use DrevOps\Robo\TokenTrait; /** * Class ForcePushTest. @@ -11,7 +13,7 @@ * * @covers \DrevOps\Robo\TokenTrait */ -class TokenTest extends AbstractIntegrationTestCase +class TokenTest extends AbstractFunctionalTestCase { /** @@ -19,9 +21,9 @@ class TokenTest extends AbstractIntegrationTestCase */ public function testTokenProcess(string $string, string $name, string $replacement, string $expectedString): void { - $mock = $this->prepareMock('DrevOps\Robo\TokenTrait', [ - 'getToken'.ucfirst($name) => function ($prop) use ($replacement) { - return !empty($prop) ? $replacement.' with property '.$prop : $replacement; + $mock = $this->prepareMock(TokenTrait::class, [ + 'getToken'.ucfirst($name) => static function (?string $prop) use ($replacement) : string { + return empty($prop) ? $replacement : $replacement.' with property '.$prop; }, ]); @@ -106,7 +108,7 @@ public static function dataProviderTokenProcess(): array */ public function testHasToken(string $string, bool $hasToken): void { - $mock = $this->prepareMock('DrevOps\Robo\TokenTrait'); + $mock = $this->prepareMock(TokenTrait::class); $actual = $this->callProtectedMethod($mock, 'hasToken', [$string]); $this->assertEquals($hasToken, $actual); diff --git a/tests/CommandTrait.php b/tests/Traits/CommandTrait.php similarity index 93% rename from tests/CommandTrait.php rename to tests/Traits/CommandTrait.php index 39f99d9..550b12a 100644 --- a/tests/CommandTrait.php +++ b/tests/Traits/CommandTrait.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests; +namespace DrevOps\Robo\Tests\Traits; use DrevOps\Robo\Tests\Exception\ErrorException; use PHPUnit\Framework\AssertionFailedError; @@ -153,8 +153,8 @@ protected function gitGetCommitsHashesFromRange(array $range, string $path = nul { $commits = $this->gitGetAllCommits($path); - array_walk($range, function (&$v) { - $v -= 1; + array_walk($range, static function (&$v) : void { + --$v; }); $ret = []; @@ -249,15 +249,15 @@ protected function gitCheckout(string $path, string $branch): void { try { $this->runGitCommand(sprintf('checkout %s', $branch), $path); - } catch (ErrorException $error) { + } catch (ErrorException $errorException) { $allowedFails = [ - "error: pathspec '$branch' did not match any file(s) known to git", + sprintf("error: pathspec '%s' did not match any file(s) known to git", $branch), ]; - $output = explode(PHP_EOL, ($error->getPrevious() instanceof \Throwable) ? $error->getPrevious()->getMessage() : ''); + $output = explode(PHP_EOL, ($errorException->getPrevious() instanceof \Throwable) ? $errorException->getPrevious()->getMessage() : ''); // Re-throw exception if it is not one of the allowed ones. if (empty(array_intersect($output, $allowedFails))) { - throw $error; + throw $errorException; } } } @@ -289,7 +289,7 @@ protected function gitReset($path): void */ protected function gitCreateFixtureFile(string $path, string $name = '', $content = ''): string { - $name = $name ? $name : 'tmp'.rand(1000, 100000); + $name = $name !== '' && $name !== '0' ? $name : 'tmp'.rand(1000, 100000); $path = $path.DIRECTORY_SEPARATOR.$name; $dir = dirname($path); if (!empty($dir)) { @@ -397,7 +397,7 @@ protected function gitAssertFilesNotExist(string $path, $files, string $branch = */ protected function gitAssertFilesCommitted(string $path, $expectedFiles, string $branch = ''): void { - if ($branch) { + if ($branch !== '' && $branch !== '0') { $this->gitCheckout($path, $branch); } $expectedFiles = is_array($expectedFiles) ? $expectedFiles : [$expectedFiles]; @@ -417,7 +417,7 @@ protected function gitAssertFilesCommitted(string $path, $expectedFiles, string */ protected function gitAssertNoFilesCommitted(string $path, $expectedFiles, string $branch = ''): void { - if ($branch) { + if ($branch !== '' && $branch !== '0') { $this->gitCheckout($path, $branch); } $expectedFiles = is_array($expectedFiles) ? $expectedFiles : [$expectedFiles]; @@ -512,11 +512,11 @@ public function runRoboCommand(string $command, bool $expectFail = false, string if ($expectFail) { throw new AssertionFailedError('Command exited successfully but should not'); } - } catch (ErrorException $error) { + } catch (ErrorException $errorException) { if (!$expectFail) { - throw $error; + throw $errorException; } - $output = explode(PHP_EOL, ($error->getPrevious() instanceof \Throwable) ? $error->getPrevious()->getMessage() : ''); + $output = explode(PHP_EOL, ($errorException->getPrevious() instanceof \Throwable) ? $errorException->getPrevious()->getMessage() : ''); } return $output; @@ -551,17 +551,19 @@ protected function runCliCommand(string $command): array return $output; } - /** - * Asserts that two associative arrays are similar. - * - * Both arrays must have the same indexes with identical values - * without respect to key ordering - * - * @param array $expected - * @param array $array - * - * @phpstan-ignore-next-line - */ + /** + * Asserts that two associative arrays are similar. + * + * Both arrays must have the same indexes with identical values + * without respect to key ordering + * + * @param array $expected + * Expected assert. + * @param array $array + * The array want to assert. + * + * @phpstan-ignore-next-line + */ protected function assertArraySimilar(array $expected, array $array): void { $this->assertEquals([], array_diff($array, $expected)); diff --git a/tests/Traits/MockTrait.php b/tests/Traits/MockTrait.php new file mode 100644 index 0000000..54ead3a --- /dev/null +++ b/tests/Traits/MockTrait.php @@ -0,0 +1,77 @@ + $methodsMap + * Optional array of methods and values, keyed by method name. Array + * elements can be return values, callbacks created with + * $this->returnCallback(), or closures. + * @param array $args + * Optional array of constructor arguments. If omitted, a constructor + * will not be called. + * + * @return object + * Mocked class. + * + * @throws \ReflectionException + */ + protected function prepareMock(string $class, array $methodsMap = [], array $args = []) + { + $methods = array_keys($methodsMap); + + $reflectionClass = new \ReflectionClass($class); + + if ($reflectionClass->isAbstract()) { + $mock = $this->getMockForAbstractClass($class, $args, '', !empty($args), true, true, $methods); + } elseif ($reflectionClass->isTrait()) { + $mock = $this->getMockForTrait($class, [], '', true, true, true, array_keys($methodsMap)); + } else { + $mockBuilder = $this->getMockBuilder($class); + if (!empty($args)) { + $mockBuilder = $mockBuilder->enableOriginalConstructor() + ->setConstructorArgs($args); + } else { + $mockBuilder = $mockBuilder->disableOriginalConstructor(); + } + /* @todo setMethods method is not found on MockBuilder */ + /* @phpstan-ignore-next-line */ + $mock = $mockBuilder->setMethods($methods) + ->getMock(); + } + + foreach ($methodsMap as $method => $value) { + // Handle callback values differently. + if (is_object($value) && str_contains($value::class, 'Callback')) { + $mock->expects($this->any()) + ->method($method) + ->will($value); + } elseif (is_object($value) && str_contains($value::class, 'Closure')) { + $mock->expects($this->any()) + ->method($method) + ->will($this->returnCallback($value)); + } else { + $mock->expects($this->any()) + ->method($method) + ->willReturn($value); + } + } + + return $mock; + } +} diff --git a/tests/Traits/ReflectionTrait.php b/tests/Traits/ReflectionTrait.php new file mode 100644 index 0000000..46518f0 --- /dev/null +++ b/tests/Traits/ReflectionTrait.php @@ -0,0 +1,103 @@ +hasMethod($name)) { + throw new \InvalidArgumentException(sprintf('Method %s does not exist', $name)); + } + + $method = $class->getMethod($name); + + $originalAccessibility = $method->isPublic(); + + // Set method accessibility to true, so it can be invoked. + $method->setAccessible(true); + + // If the method is static, we won't pass an object instance to invokeArgs() + // Otherwise, we ensure to pass the object instance. + $invokeObject = $method->isStatic() ? null : (is_object($object) ? $object : null); + + // Ensure we have an object for non-static methods. + if (!$method->isStatic() && $invokeObject === null) { + throw new \InvalidArgumentException("An object instance is required for non-static methods"); + } + + $result = $method->invokeArgs($invokeObject, $args); + + // Reset the method's accessibility to its original state. + $method->setAccessible($originalAccessibility); + + return $result; + } + + /** + * Set protected property value. + * + * @param object $object + * Object to set the value on. + * @param string $property + * Property name to set the value. Property should exists in the object. + * @param mixed $value + * Value to set to the property. + */ + protected static function setProtectedValue($object, $property, mixed $value): void + { + $class = new \ReflectionClass($object::class); + $property = $class->getProperty($property); + $property->setAccessible(true); + + $property->setValue($object, $value); + } + + /** + * Get protected value from the object. + * + * @param object $object + * Object to set the value on. + * @param string $property + * Property name to get the value. Property should exists in the object. + * + * @return mixed + * Protected property value. + */ + protected static function getProtectedValue($object, $property) + { + $class = new \ReflectionClass($object::class); + $property = $class->getProperty($property); + $property->setAccessible(true); + + return $property->getValue($class); + } +} diff --git a/tests/Unit/AbstractUnitTestCase.php b/tests/Unit/AbstractUnitTestCase.php index 1970a81..a05e3c2 100644 --- a/tests/Unit/AbstractUnitTestCase.php +++ b/tests/Unit/AbstractUnitTestCase.php @@ -2,6 +2,7 @@ namespace DrevOps\Robo\Tests\Unit; +use DrevOps\Robo\ArtefactTrait; use DrevOps\Robo\Tests\AbstractTestCase; /** @@ -21,7 +22,7 @@ protected function setUp(): void { parent::setUp(); - $this->mock = $this->getMockForTrait('DrevOps\Robo\ArtefactTrait'); + $this->mock = $this->getMockForTrait(ArtefactTrait::class); $this->callProtectedMethod($this->mock, 'fsSetRootDir', [$this->fixtureDir]); } } diff --git a/tests/Unit/ExcludeTest.php b/tests/Unit/ExcludeTest.php index f6436b9..3e7c602 100644 --- a/tests/Unit/ExcludeTest.php +++ b/tests/Unit/ExcludeTest.php @@ -35,7 +35,6 @@ public function testExcludeExists(): void * @param bool $expected * Expected. * - * @return void * * @dataProvider dataProviderExcludeEmpty *