diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 6681ae3..5cddb3e 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -1,6 +1,7 @@ name: Continuous Integration on: push: + workflow_dispatch: schedule: - cron: 0 0 * * 0 @@ -10,8 +11,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - os: [unbuntu-latest, macos-latest, windows-latest] - php: ['8.1', '8.2'] + os: [ubuntu-latest, macos-latest, windows-latest] + php: ['8.2', '8.3', '8.4'] steps: - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -23,17 +24,40 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php }}-composer- + - name: Install Dependencies run: composer install - if: ${{ matrix.php != '8.2' }} - - - name: Install Dependencies (Ignore Platform Req) - run: composer install --ignore-platform-req=php - if: ${{ matrix.php == '8.2' }} - name: Test run: vendor/bin/phpunit tests --testdox --coverage-clover coverage.xml + - name: Upload Coverage Artifact + uses: actions/upload-artifact@v4 + if: ${{ matrix.php == '8.4' && matrix.os == 'ubuntu-latest' }} + with: + name: codecov + path: coverage.xml + + codecov: + name: Code Coverage Report + runs-on: ubuntu-latest + needs: phpunit + steps: + - name: Download Coverage Artifact + uses: actions/download-artifact@v4 + with: + name: codecov + - name: Upload to CodeCov uses: codecov/codecov-action@v1 with: @@ -46,7 +70,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2'] + php: ['8.2', '8.3', '8.4'] steps: - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -57,13 +81,19 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php }}-composer- + - name: Install Dependencies run: composer install - if: ${{ matrix.php != '8.2' }} - - - name: Install Dependencies (Ignore Platform Req) - run: composer install --ignore-platform-req=php - if: ${{ matrix.php == '8.2' }} - name: Run PHPStan run: vendor/bin/phpstan @@ -75,19 +105,25 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.4 tools: composer:v2 - name: Checkout Repository uses: actions/checkout@v2 + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php }}-composer- + - name: Install Dependencies run: composer install - if: ${{ matrix.php != '8.2' }} - - - name: Install Dependencies (Ignore Platform Req) - run: composer install --ignore-platform-req=php - if: ${{ matrix.php == '8.2' }} - name: PHP Code Sniffer run: vendor/bin/phpcs @@ -98,7 +134,7 @@ jobs: continue-on-error: true strategy: matrix: - php: ['8.1', '8.2'] + php: ['8.2', '8.3', '8.4'] steps: - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -111,13 +147,19 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php }}-composer- + - name: Install Dependencies run: composer install - if: ${{ matrix.php != '8.2' }} - - - name: Install Dependencies (Ignore Platform Req) - run: composer install --ignore-platform-req=php - if: ${{ matrix.php == '8.2' }} - name: Check for Mutants env: diff --git a/composer.json b/composer.json index 3d8beeb..011e664 100644 --- a/composer.json +++ b/composer.json @@ -21,13 +21,18 @@ } }, "require": { - "php": "^8.1" + "php": "^8.2" + }, + "config": { + "allow-plugins": { + "infection/extension-installer": true + } }, "require-dev": { "jetbrains/phpstorm-attributes": "^1", "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^1", - "infection/infection": "^0.26.0", + "phpstan/phpstan": "^2", + "infection/infection": "^0.31.0", "squizlabs/php_codesniffer": "^3.6", "roave/security-advisories": "dev-master" } diff --git a/phpstan.neon b/phpstan.neon index 3e639cd..776ccd8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,4 @@ parameters: - level: 9 + level: max paths: - src diff --git a/src/SmartString.php b/src/SmartString.php index 2325c82..b8a7bd2 100644 --- a/src/SmartString.php +++ b/src/SmartString.php @@ -1,5 +1,7 @@ shouldUseGrapheme) { true => grapheme_stripos($this->value, $needle, $offset), false => stripos($this->value, $needle, $offset) @@ -89,7 +92,7 @@ public function strrpos(Stringable|string $needle, int $offset = 0): int|false #[Pure] public function findLastPosition(Stringable|string $needle, int $offset = 0, int $flags = 0): int|false { - if (($flags & static::CASE_INSENSITIVE) == static::CASE_INSENSITIVE) { + if (($flags & self::CASE_INSENSITIVE) == self::CASE_INSENSITIVE) { return $this->strripos($needle, $offset); } return $this->strrpos($needle, $offset); @@ -124,7 +127,7 @@ public function strpos(Stringable|string $needle, int $offset = 0): int|false #[Pure] public function findPosition(Stringable|string $needle, int $offset = 0, int $flags = 0): int|false { - if (($flags & static::CASE_INSENSITIVE) == static::CASE_INSENSITIVE) { + if (($flags & self::CASE_INSENSITIVE) == self::CASE_INSENSITIVE) { return $this->stripos($needle, $offset); } return $this->strpos($needle, $offset); @@ -144,13 +147,23 @@ public function strstr(Stringable|string $needle, bool $beforeNeedle = false): S #[Pure] public function substringFromNeedle( Stringable|string $needle, - bool $beforeNeedle = false, int $flags = 0 ): SmartString|false { - if (($flags & static::CASE_INSENSITIVE) == static::CASE_INSENSITIVE) { - return $this->stristr($needle, $beforeNeedle); + if (($flags & self::CASE_INSENSITIVE) == self::CASE_INSENSITIVE) { + return $this->stristr($needle); + } + return $this->strstr($needle); + } + + #[Pure] + public function substringUntilNeedle( + Stringable|string $needle, + int $flags = 0 + ): SmartString|false { + if (($flags & self::CASE_INSENSITIVE) == self::CASE_INSENSITIVE) { + return $this->stristr($needle, true); } - return $this->strstr($needle, $beforeNeedle); + return $this->strstr($needle, true); } #[Pure] diff --git a/tests/ConcatenateTest.php b/tests/ConcatenateTest.php new file mode 100644 index 0000000..6d8e9b7 --- /dev/null +++ b/tests/ConcatenateTest.php @@ -0,0 +1,51 @@ +factory = new SmartStringFactory(); + parent::setUp(); + } + + public function getData(): array + { + return [ + ['a', 'b', 'ab'], + ['a', SmartString::build('b'), 'ab'], + ['a', '🏴', 'a🏴'], + ['🏴', 'a', '🏴a'], + ]; + } + + /** + * @dataProvider getData + */ + public function testConcat(string $initial, Stringable|string $additional, string $expectedResult): void + { + $object = $this->factory->create($initial); + $result = $object->concat($additional); + $this->assertInstanceOf(SmartString::class, $result); + $this->assertEquals($expectedResult, (string)$result); + } + + /** + * @dataProvider getData + */ + public function testConcatenate(string $initial, Stringable|string $additional, string $expectedResult): void + { + $object = $this->factory->create($initial); + $result = $object->concatenate($additional); + $this->assertInstanceOf(SmartString::class, $result); + $this->assertEquals($expectedResult, (string)$result); + } +} diff --git a/tests/ConstructionTest.php b/tests/ConstructionTest.php new file mode 100644 index 0000000..688fb07 --- /dev/null +++ b/tests/ConstructionTest.php @@ -0,0 +1,64 @@ +factory = new SmartStringFactory(); + } + + public function getData(): array + { + return [ + ['a', 'a', false], + [SmartString::build('a'), 'a', false], + ['🏴', '🏴', true], + [SmartString::build('🏴'), '🏴', true], + ]; + } + + /** + * @dataProvider getData + */ + public function testBuild( + Stringable|string $suppliedValue, + string $expectedValue, + bool $expectedUseGraphemeValue + ): void { + $object = SmartString::build($suppliedValue); + $this->testObject($object, $expectedValue, $expectedUseGraphemeValue); + } + + /** + * @dataProvider getData + */ + public function testFactory( + Stringable|string $suppliedValue, + string $expectedValue, + bool $expectedUseGraphemeValue + ): void { + $object = $this->factory->create($suppliedValue); + $this->testObject($object, $expectedValue, $expectedUseGraphemeValue); + } + + private function testObject(SmartString $object, string $expectedValue, bool $expectedUseGraphemeValue) + { + $reflectionObject = new ReflectionClass($object); + + $valueProp = $reflectionObject->getProperty('value'); + $shouldUseGraphemeProp = $reflectionObject->getProperty('shouldUseGrapheme'); + + $this->assertEquals($expectedValue, $valueProp->getValue($object)); + $this->assertEquals($expectedUseGraphemeValue, $shouldUseGraphemeProp->getValue($object)); + } +} diff --git a/tests/FindPositionTest.php b/tests/FindPositionTest.php new file mode 100644 index 0000000..05724fa --- /dev/null +++ b/tests/FindPositionTest.php @@ -0,0 +1,99 @@ +factory = new SmartStringFactory(); + parent::setUp(); + } + + private function getDataCaseSensitive(): array + { + return [ + ['abcABCdefDEF', 'c', 2], + ['abcabcdefdef', 'c', 5, 3], + ['abcABCdefDEF', SmartString::build('c'), 2], + ['abcabcdefdef', SmartString::build('c'), 5, 3], + ['ab🏴defg', '🏴', 2], + ['ab🏴defg', SmartString::build('🏴'), 2], + ]; + } + + private function getDataCaseInsensitive(): array + { + return [ + ['abcABCdefDEF', 'C', 2], + ['ABCabcABCabc', 'C', 5, 3], + ['abcABCdefDEF', SmartString::build('C'), 2], + ['ABCabcABCabc', SmartString::build('C'), 5, 3], + ['ab🏴defg', '🏴', 2], + ['ab🏴defg', SmartString::build('🏴'), 2], + ]; + } + + /** + * @dataProvider getDataCaseSensitive + */ + public function testFindPositionCaseSensitive( + Stringable|string $initial, + Stringable|string $needle, + int|bool $expected, + int $offset = 0 + ): void { + $object = $this->factory->create($initial); + $result = $object->findPosition($needle, $offset); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseInsensitive + */ + public function testFindPositionCaseInsensitive( + Stringable|string $initial, + Stringable|string $needle, + int|bool $expected, + int $offset = 0 + ): void { + $object = $this->factory->create($initial); + $result = $object->findPosition($needle, $offset, SmartString::CASE_INSENSITIVE); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseInsensitive + */ + public function testStripos( + Stringable|string $initial, + Stringable|string $needle, + int|bool $expected, + int $offset = 0 + ): void { + $object = $this->factory->create($initial); + $result = $object->stripos($needle, $offset); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseSensitive + */ + public function testStrpos( + Stringable|string $initial, + Stringable|string $needle, + int|bool $expected, + int $offset = 0 + ): void { + $object = $this->factory->create($initial); + $result = $object->strpos($needle, $offset); + $this->assertEquals($expected, $result); + } +} diff --git a/tests/StringLengthTest.php b/tests/StringLengthTest.php index 75fca03..849b6ac 100644 --- a/tests/StringLengthTest.php +++ b/tests/StringLengthTest.php @@ -2,8 +2,10 @@ namespace Navarr\SmartString\Test; +use Navarr\SmartString\SmartString; use Navarr\SmartString\SmartStringFactory; use PHPUnit\Framework\TestCase; +use Stringable; class StringLengthTest extends TestCase { @@ -20,14 +22,15 @@ public function getData(): array return [ ['abcdef', 6], ['こんにちは', 5], - ['🏴󠁧󠁢󠁥󠁮󠁧󠁿', 1] + ['🏴󠁧󠁢󠁥󠁮󠁧󠁿', 1], + [SmartString::build('abc'), 3] // Ensure methods properly handle a Stringable ]; } /** * @dataProvider getData */ - public function testLength(string $string, int $expectedLength) + public function testLength(Stringable|string $string, int $expectedLength) { $testString = $this->factory->create($string); $this->assertEquals($expectedLength, $testString->length()); @@ -36,7 +39,7 @@ public function testLength(string $string, int $expectedLength) /** * @dataProvider getData */ - public function testStrlen(string $string, int $expectedLength) + public function testStrlen(Stringable|string $string, int $expectedLength) { $testString = $this->factory->create($string); $this->assertEquals($expectedLength, $testString->strlen()); diff --git a/tests/SubstringFromNeedleTest.php b/tests/SubstringFromNeedleTest.php new file mode 100644 index 0000000..ab9f9e2 --- /dev/null +++ b/tests/SubstringFromNeedleTest.php @@ -0,0 +1,125 @@ +factory = new SmartStringFactory(); + parent::setUp(); + } + + private function getDataCaseInsensitive(): array + { + return [ + ['abcdefg', 'c', 'cdefg'], + ['abcdefg', SmartString::build('c'), 'cdefg'], + ['abcABCdefDEF', 'C', 'cABCdefDEF'], + ['abcABCdefDEF', SmartString::build('C'), 'cABCdefDEF'], + ['abcdefg', 'h', false], + ['abcdefg', SmartString::build('h'), false], + ['abcdefg', '🏴', false], + ['abcdefg', SmartString::build('🏴'), false], + ['ab🏴defg', '🏴', '🏴defg'], + ['ab🏴defg', SmartString::build('🏴'), '🏴defg'], + ]; + } + + private function getDataCaseSensitive(): array + { + return [ + ['abcABCdefDEF', 'C', 'CdefDEF'], + ['abcABCdefDEF', SmartString::build('C'), 'CdefDEF'], + ['abcABCdefDEF', 'c', 'cABCdefDEF'], + ['abcABCdefDEF', SmartString::build('c'), 'cABCdefDEF'], + ['abcABCdefDEF', 'h', false], + ['abcABCdefDEF', SmartString::build('h'), false], + ['abcABCdefDEF', 'H', false], + ['abcABCdefDEF', SmartString::build('H'), false], + ['abcABCdefDEF', '🏴', false], + ['abcABCdefDEF', SmartString::build('🏴'), false], + ['abcABC🏴defDEF', '🏴', '🏴defDEF'], + ['abcABC🏴defDEF', SmartString::build('🏴'), '🏴defDEF'], + ]; + } + + /** + * @dataProvider getDataCaseSensitive + */ + public function testSubstringFromNeedleCaseSensitive( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->substringFromNeedle($substring); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseSensitive + */ + public function testStrstrWithBeforeNeedleFalse( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->strstr($substring); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseInsensitive + */ + public function testSubstringFromNeedleCaseInsensitive( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->substringFromNeedle($substring, SmartString::CASE_INSENSITIVE); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseInsensitive + */ + public function testStristrWithBeforeNeedleFalse( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->stristr($substring); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } +} diff --git a/tests/SubstringUntilNeedleTest.php b/tests/SubstringUntilNeedleTest.php new file mode 100644 index 0000000..7e5a560 --- /dev/null +++ b/tests/SubstringUntilNeedleTest.php @@ -0,0 +1,125 @@ +factory = new SmartStringFactory(); + parent::setUp(); + } + + /** + * @dataProvider getDataCaseInsensitive + */ + public function testStristrWithBeforeNeedleTrue( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->stristr($substring, true); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseSensitive + */ + public function testStrstrWithBeforeNeedleTrue( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->strstr($substring, true); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseInsensitive + */ + public function testSubstringUntilNeedleCaseInsensitive( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->substringUntilNeedle($substring, SmartString::CASE_INSENSITIVE); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getDataCaseSensitive + */ + public function testSubstringUntilNeedleCaseSensitive( + Stringable|string $initial, + Stringable|string $substring, + string|false $expected + ): void { + $object = $this->factory->create($initial); + $result = $object->substringUntilNeedle($substring); + + if (is_string($expected)) { + $this->assertInstanceOf(SmartString::class, $result); + $result = (string)$result; + } + $this->assertEquals($expected, $result); + } +}