diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9ec3ccc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,49 @@ +version: 2 + +# Grouped: a week of transitive bumps arrives as one PR, not one per package. +updates: + - package-ecosystem: composer + directory: "/" + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + # This is a library. Widen the declared range, and keep the old versions + # that consumers still resolve. + versioning-strategy: widen + # Let the ecosystem vet a release before we take it. Security updates ignore + # cooldown, so an advisory fix still arrives at once. + cooldown: + default-days: 7 + semver-patch-days: 5 + semver-minor-days: 14 + semver-major-days: 30 + groups: + # One PR for all open advisories, prod and dev. + composer-security: + applies-to: security-updates + patterns: ["*"] + # require-dev bumps reach nobody who installs the SDK. + composer-development: + applies-to: version-updates + dependency-type: development + patterns: ["*"] + # Majors stay ungrouped. They need a composer.json change and a release note. + composer-production: + applies-to: version-updates + dependency-type: production + patterns: ["*"] + update-types: ["minor", "patch"] + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + # The GitHub Actions ecosystem supports default-days only. + cooldown: + default-days: 7 + groups: + github-actions: + applies-to: version-updates + patterns: ["*"] diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1a487a4..e48617e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,6 +10,10 @@ on: pull_request: + # Catches upstream releases that break our declared ranges. + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: workflow_call: @@ -28,9 +32,25 @@ permissions: contents: read jobs: - build-and-test: + test-matrix: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # The locked legs install what composer.lock pins. The lowest and + # highest legs resolve, and each Symfony major raises its PHP floor, + # so the runner PHP picks the branch of the symfony/cache constraint: + # 8.1 takes ^6.4, 8.3 takes ^7.0, 8.4 takes ^8.0. + - { php: '8.1', deps: locked } + - { php: '8.3', deps: locked } + - { php: '8.1', deps: lowest } + - { php: '8.1', deps: highest } + - { php: '8.3', deps: highest } + - { php: '8.4', deps: highest } + + name: PHP ${{ matrix.php }}, ${{ matrix.deps }} steps: - name: Display Testing Details @@ -38,26 +58,59 @@ jobs: echo "Running SDK Test using" echo "Test Data: sdk-test-data@${TEST_DATA_BRANCH_NAME}" echo "SDK Branch: php-sdk@${SDK_BRANCH_NAME}" + echo "PHP: ${{ matrix.php }}, dependencies: ${{ matrix.deps }}" - uses: actions/checkout@v5 with: - repository: ${{ github.event.pull_request.head.repo.full_name || 'Eppo-exp/php-sdk' }} + # A called workflow sees the caller's github context, so github.repository + # is the caller's repo. Name this repo to stay correct on both paths. + repository: ${{ github.repository == 'Eppo-exp/php-sdk' && github.event.pull_request.head.repo.full_name || 'Eppo-exp/php-sdk' }} ref: ${{ env.SDK_BRANCH_NAME }} + # No step needs git auth. make test clones test data from a public repo. + persist-credentials: false + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, pcntl, posix, sockets + coverage: none + + # Must run before any resolution, or --strict cannot check lock sync. - name: Validate composer.json and composer.lock run: composer validate --strict - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- + # config.platform.php guards composer.lock. It must not cap a throwaway + # resolution, or symfony/cache ^8 stays unreachable. + - name: Target the runner's PHP for highest-version resolution + if: matrix.deps == 'highest' + run: composer config --unset platform.php - name: Install dependencies - run: composer install --prefer-dist --no-progress + uses: ramsey/composer-install@4.0.0 + with: + dependency-versions: ${{ matrix.deps }} + composer-options: --prefer-dist + + - name: Show resolved direct dependencies + run: composer show --direct + + # --no-dev: a dev-only advisory reaches nobody who installs the SDK. + - name: Audit production dependencies + if: matrix.deps == 'locked' + run: composer audit --no-dev - name: Run tests run: make test branchName=${{env.TEST_DATA_BRANCH_NAME}} + + # One stable check for every leg. Branch protection keeps the name + # build-and-test, whatever the matrix holds. + build-and-test: + if: always() + needs: test-matrix + runs-on: ubuntu-latest + steps: + - name: Check the matrix result + run: | + echo "Matrix result: ${{ needs.test-matrix.result }}" + [ "${{ needs.test-matrix.result }}" = "success" ] diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 9ed4871..8d70290 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -3,7 +3,6 @@ name: Package Integration Testing on: push: branches: [ main ] - create: tags: - v* diff --git a/Makefile b/Makefile index 1e10e49..629ee6b 100644 --- a/Makefile +++ b/Makefile @@ -44,4 +44,4 @@ test-data: .PHONY: test test: test-data - ./vendor/phpunit/phpunit/phpunit tests + ./vendor/bin/phpunit tests diff --git a/composer.json b/composer.json index a8f6689..e235122 100644 --- a/composer.json +++ b/composer.json @@ -26,11 +26,10 @@ "ext-json": "*", "psr/simple-cache": "3.*", "psr/log": "^2.0|^3.0", - "shrikeh/teapot": "^2.3", "composer/semver": "^3.4", "php-http/discovery": "^1.17", "webclient/ext-redirect": "^2.0", - "symfony/cache": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0" }, "require-dev": { "phpunit/phpunit": "^9.6", @@ -39,7 +38,8 @@ "ext-pcntl": "*", "psr-mock/http": "^1.0", "squizlabs/php_codesniffer": "^3.10", - "ext-sockets": "*" + "ext-sockets": "*", + "teapot/status-code": "^2.1" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 6e6e6ac..7adbfd9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b3ee69a71ef7626dbb25f67a4c582f91", + "content-hash": "59028d5f34d76c9e5fb1f62a317cf28e", "packages": [ { "name": "composer/semver", @@ -470,63 +470,6 @@ }, "time": "2021-10-29T13:26:27+00:00" }, - { - "name": "shrikeh/teapot", - "version": "v2.3.1", - "source": { - "type": "git", - "url": "https://github.com/shrikeh/teapot.git", - "reference": "2fea5720963f22eedb920ee65b9d643bfa3e8daf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/shrikeh/teapot/zipball/2fea5720963f22eedb920ee65b9d643bfa3e8daf", - "reference": "2fea5720963f22eedb920ee65b9d643bfa3e8daf", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "psr/http-message": "^1.0", - "teapot/status-code": "^1.0" - }, - "require-dev": { - "behat/behat": "^3.2", - "bossa/phpspec2-expect": "^2.0", - "escapestudios/symfony2-coding-standard": "^2.9", - "phpspec/phpspec": "~3.0.0", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "autoload": { - "psr-4": { - "Teapot\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barney Hanlon", - "email": "barney@shrikeh.net" - }, - { - "name": "Andreas Möller", - "email": "am@localheinz.com" - } - ], - "description": "PHP HTTP Response Status library", - "homepage": "https://shrikeh.github.io/teapot/", - "keywords": [ - "http" - ], - "support": { - "issues": "https://github.com/shrikeh/teapot/issues", - "source": "https://github.com/shrikeh/teapot/tree/master" - }, - "time": "2017-09-01T13:56:48+00:00" - }, { "name": "symfony/cache", "version": "v6.4.40", @@ -946,53 +889,6 @@ ], "time": "2026-04-14T12:12:40+00:00" }, - { - "name": "teapot/status-code", - "version": "v1.1.2", - "source": { - "type": "git", - "url": "https://github.com/teapot-php/status-code.git", - "reference": "7aaba4364645ebb9d3a36267c9b3a32206d3042b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/teapot-php/status-code/zipball/7aaba4364645ebb9d3a36267c9b3a32206d3042b", - "reference": "7aaba4364645ebb9d3a36267c9b3a32206d3042b", - "shasum": "" - }, - "require": { - "php": "^5.4 || ^7.0 || ^8.0" - }, - "require-dev": { - "squizlabs/php_codesniffer": "^3.5.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "Teapot\\StatusCode\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barney Hanlon", - "email": "barney@shrikeh.net" - } - ], - "description": "PHP HTTP Response Status code library", - "homepage": "http://shrikeh.github.com/teapot", - "keywords": [ - "http" - ], - "support": { - "issues": "https://github.com/teapot-php/status-code/issues", - "source": "https://github.com/teapot-php/status-code/tree/v1.1.2" - }, - "time": "2020-11-03T17:14:32+00:00" - }, { "name": "webclient/ext-redirect", "version": "v2.0.1", @@ -4640,6 +4536,61 @@ ], "time": "2026-04-10T16:19:22+00:00" }, + { + "name": "teapot/status-code", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/teapot-php/status-code.git", + "reference": "37668b9c1dd756e54bca489fb5c62c14223cb87e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/teapot-php/status-code/zipball/37668b9c1dd756e54bca489fb5c62c14223cb87e", + "reference": "37668b9c1dd756e54bca489fb5c62c14223cb87e", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.48.2", + "ergebnis/php-cs-fixer-config": "^6.56.1", + "phpstan/phpstan": "^2.1.32", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-strict-rules": "^2.0.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Teapot\\StatusCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barney Hanlon", + "email": "barney@shrikeh.net" + }, + { + "name": "Andreas Möller", + "email": "am@localheinz.com" + } + ], + "description": "PHP HTTP Response Status code library", + "homepage": "https://github.com/teapot-php/status-code", + "keywords": [ + "http" + ], + "support": { + "issues": "https://github.com/teapot-php/status-code/issues", + "source": "https://github.com/teapot-php/status-code/tree/2.5.0" + }, + "time": "2025-11-20T08:53:39+00:00" + }, { "name": "theseer/tokenizer", "version": "1.3.1", diff --git a/src/API/APIRequestWrapper.php b/src/API/APIRequestWrapper.php index d83ec23..b6ac221 100644 --- a/src/API/APIRequestWrapper.php +++ b/src/API/APIRequestWrapper.php @@ -7,7 +7,6 @@ use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; -use Teapot\StatusCode\RFC\RFC7231; use Webclient\Extension\Redirect\RedirectClientDecorator; /** @@ -24,6 +23,14 @@ class APIRequestWrapper private const BANDIT_ENDPOINT = '/flag-config/v1/bandits'; private const CONFIG_BASE = 'https://fscdn.eppo.cloud/api'; + /** HTTP status codes, named per RFC 7231, RFC 7232, and RFC 7235. */ + private const HTTP_NOT_MODIFIED = 304; + private const HTTP_BAD_REQUEST = 400; + private const HTTP_UNAUTHORIZED = 401; + private const HTTP_REQUEST_TIMEOUT = 408; + private const HTTP_CONFLICT = 409; + private const HTTP_INTERNAL_SERVER_ERROR = 500; + private string $baseUrl; public bool $isUnauthorized = false; @@ -69,13 +76,14 @@ private function getResource(string $endpoint, ?string $lastETag = null): APIRes $response = $this->httpClient->sendRequest($request); } catch (ClientExceptionInterface $e) { - throw new HttpRequestException($e, 0, false); + // Keep the chain. The message alone loses the client exception. + throw new HttpRequestException($e->getMessage(), 0, false, $e); } - if ($response->getStatusCode() >= 400) { - $this->handleHttpError($response->getStatusCode(), $response->getBody()); + if ($response->getStatusCode() >= self::HTTP_BAD_REQUEST) { + $this->handleHttpError($response->getStatusCode(), (string)$response->getBody()); } - if ($response->getStatusCode() == 304) { // Not modified + if ($response->getStatusCode() == self::HTTP_NOT_MODIFIED) { // Quick Return return new APIResource(null, false, $lastETag); } @@ -121,7 +129,7 @@ public function getBandits(): APIResource */ private function handleHttpError(int $status, string $error) { - $this->isUnauthorized = $status === 401; + $this->isUnauthorized = $status === self::HTTP_UNAUTHORIZED; $isRecoverable = $this->isHttpErrorRecoverable($status); if ($this->isUnauthorized) { throw new InvalidApiKeyException(); @@ -137,8 +145,8 @@ private function handleHttpError(int $status, string $error) */ private function isHttpErrorRecoverable(int $status): bool { - if ($status >= RFC7231::BAD_REQUEST && $status < RFC7231::INTERNAL_SERVER_ERROR) { - return $status === RFC7231::CONFLICT || $status === RFC7231::REQUEST_TIMEOUT; + if ($status >= self::HTTP_BAD_REQUEST && $status < self::HTTP_INTERNAL_SERVER_ERROR) { + return $status === self::HTTP_CONFLICT || $status === self::HTTP_REQUEST_TIMEOUT; } return true; } diff --git a/src/Bandits/BanditEvaluator.php b/src/Bandits/BanditEvaluator.php index 51e59d0..e41b12c 100644 --- a/src/Bandits/BanditEvaluator.php +++ b/src/Bandits/BanditEvaluator.php @@ -6,6 +6,7 @@ use Eppo\DTO\Bandit\AttributeSet; use Eppo\DTO\Bandit\BanditEvaluation; use Eppo\DTO\Bandit\BanditModelData; +use Eppo\DTO\Bandit\CategoricalAttributeCoefficient; use Eppo\DTO\Bandit\NumericAttributeCoefficient; use Eppo\Exception\BanditEvaluationException; use Eppo\Exception\InvalidArgumentException; @@ -221,19 +222,21 @@ public static function scoreNumericAttributes(array $coefficients, array $attrib return $score; } + /** + * @param array $coefficients + * @param array $attributes + * @return float + */ public static function scoreCategoricalAttributes(array $coefficients, array $attributes): float { $score = 0.0; foreach ($coefficients as $coefficient) { $attributeKey = $coefficient->attributeKey; $valueCoefficients = $coefficient->valueCoefficients; - if ( - array_key_exists($attributeKey, $attributes) && array_key_exists( - $attributes[$attributeKey], - $valueCoefficients - ) - ) { - $score += $valueCoefficients[$attributes[$attributeKey]]; + $attributeValue = $attributes[$attributeKey] ?? null; + // A null value scores as missing, like a numeric attribute does. + if ($attributeValue !== null && array_key_exists($attributeValue, $valueCoefficients)) { + $score += $valueCoefficients[$attributeValue]; } else { $score += $coefficient->missingValueCoefficient; } diff --git a/src/EppoClient.php b/src/EppoClient.php index 7230832..ef50a97 100644 --- a/src/EppoClient.php +++ b/src/EppoClient.php @@ -63,7 +63,7 @@ protected function __construct( private readonly PollerInterface $poller, private readonly ?LoggerInterface $eventLogger = null, private readonly ?bool $isGracefulMode = true, - IBanditEvaluator $banditEvaluator = null, + ?IBanditEvaluator $banditEvaluator = null, ?PsrLoggerInterface $logger = null, ) { $this->evaluator = new RuleEvaluator(); @@ -88,10 +88,10 @@ protected function __construct( public static function init( string $apiKey, ?string $baseUrl = null, - LoggerInterface $assignmentLogger = null, - CacheInterface $cache = null, - ClientInterface $httpClient = null, - RequestFactoryInterface $requestFactory = null, + ?LoggerInterface $assignmentLogger = null, + ?CacheInterface $cache = null, + ?ClientInterface $httpClient = null, + ?RequestFactoryInterface $requestFactory = null, ?bool $isGracefulMode = true, ?PollingOptions $pollingOptions = null, ?bool $throwOnFailedInit = false, @@ -371,7 +371,7 @@ private function getAssignmentDetail( string $flagKey, string $subjectKey, array $subjectAttributes = [], - VariationType $expectedVariationType = null, + ?VariationType $expectedVariationType = null, ?Configuration $config = null, ): ?Variation { Validator::validateNotBlank($subjectKey, 'Invalid argument: subjectKey cannot be blank'); diff --git a/src/Exception/BanditEvaluationException.php b/src/Exception/BanditEvaluationException.php index e5301c8..11ec8cc 100644 --- a/src/Exception/BanditEvaluationException.php +++ b/src/Exception/BanditEvaluationException.php @@ -9,7 +9,7 @@ class BanditEvaluationException extends EppoException public function __construct( string $message = "", int $code = EppoException::BANDIT_EVALUATION_FAILED_NO_ACTIONS_PROVIDED, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct($message, $code, $previous); } diff --git a/src/Exception/EppoClientException.php b/src/Exception/EppoClientException.php index 28f8f2f..f6b482b 100644 --- a/src/Exception/EppoClientException.php +++ b/src/Exception/EppoClientException.php @@ -6,7 +6,7 @@ class EppoClientException extends EppoException { - public function __construct(string $message = "", int $code = 0, Throwable $previous = null) + public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } @@ -16,7 +16,7 @@ public function __construct(string $message = "", int $code = 0, Throwable $prev * @param int|null $code * @return self */ - public static function from(Throwable $previous = null, int $code = null): self + public static function from(?Throwable $previous = null, ?int $code = null): self { return new self($previous->getMessage(), $code ?? $previous->getCode(), $previous); } diff --git a/src/Exception/HttpRequestException.php b/src/Exception/HttpRequestException.php index 5e606e6..c19722b 100644 --- a/src/Exception/HttpRequestException.php +++ b/src/Exception/HttpRequestException.php @@ -14,7 +14,7 @@ class HttpRequestException extends EppoException * @param bool $isRecoverable * @param Throwable|null $previous */ - public function __construct($message = "", $code = 0, $isRecoverable = false, Throwable $previous = null) + public function __construct($message = "", $code = 0, $isRecoverable = false, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->isRecoverable = $isRecoverable; diff --git a/src/Exception/InvalidConfigurationException.php b/src/Exception/InvalidConfigurationException.php index c2e9ba5..215d138 100644 --- a/src/Exception/InvalidConfigurationException.php +++ b/src/Exception/InvalidConfigurationException.php @@ -6,12 +6,12 @@ class InvalidConfigurationException extends EppoException { - public function __construct(string $message = "", int $code = 0, Throwable $previous = null) + public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } - public static function from(Throwable $previous = null, int $code = 0): self + public static function from(?Throwable $previous = null, int $code = 0): self { return new self($previous->getMessage(), $code, $previous); } diff --git a/tests/API/APIRequestWrapperTest.php b/tests/API/APIRequestWrapperTest.php index cecc251..4beffc8 100644 --- a/tests/API/APIRequestWrapperTest.php +++ b/tests/API/APIRequestWrapperTest.php @@ -15,6 +15,7 @@ use PsrMock\Psr7\Response; use PsrMock\Psr7\Stream; use Teapot\StatusCode\RFC\RFC7231; +use Teapot\StatusCode\RFC\RFC7232; use Teapot\StatusCode\RFC\RFC7235; class APIRequestWrapperTest extends TestCase @@ -39,7 +40,7 @@ public function testApiGetsResource(): void $body = "RESPONSE BODY"; $ETag = "00FF22EEFF"; - $http = $this->getHttpClientMock(200, $body, ["ETag" => $ETag]); + $http = $this->getHttpClientMock(RFC7231::OK, $body, ["ETag" => $ETag]); $api = new APIRequestWrapper( 'APIKEY', [], @@ -97,13 +98,13 @@ public function testRecoverableHttpError(): void public function testUnrecoverableHttpError(): void { - $this->assertStatusRecoverable(false, RFC7235::UNAUTHORIZED); + $this->assertStatusRecoverable(false, RFC7231::BAD_REQUEST); $this->assertStatusRecoverable(false, RFC7231::NOT_FOUND); } public function testResourceFetching(): void { - $http = $this->getRespondingHttpClientMock(RFC7231::OK, ''); + $http = $this->getRespondingHttpClientMock(RFC7231::OK); $api = new APIRequestWrapper( '', [], @@ -132,8 +133,6 @@ private function assertStatusRecoverable(bool $recoverable, int $status): void $this->fail('Exception not thrown'); } catch (HttpRequestException $e) { $this->assertEquals($recoverable, $e->isRecoverable); - } catch (InvalidApiKeyException $e) { - $this->assertEquals('Invalid API Key', $e->getMessage()); } } @@ -203,6 +202,7 @@ private function getRedirectingClientMock(): ClientInterface $redirectHeaders = new Headers(); $redirectHeaders->setHeader(new Header('Location', $redirectLocation)); + // 301 and 308 take different paths in the decorator. $redirectResponse = new Response(statusCode: RFC7231::MOVED_PERMANENTLY, headers: $redirectHeaders); $resourceUri = 'https://fscdn.eppo.cloud/api/flag-config/v1/config?apiKey=APIKEY'; @@ -243,11 +243,11 @@ public function testSendsLastETagAndComputesIsModified(): void $stream = new Stream($body); $mockNewResponse = (new Response( - statusCode: 200, + statusCode: RFC7231::OK, stream: $stream ))->withAddedHeader('ETag', $ETag); $mockSameResponse = (new Response( - statusCode: 304, + statusCode: RFC7232::NOT_MODIFIED, stream: null ))->withAddedHeader('ETag', $ETag); diff --git a/tests/Bandits/BanditEvaluatorTest.php b/tests/Bandits/BanditEvaluatorTest.php index aa23ff0..2e1b939 100644 --- a/tests/Bandits/BanditEvaluatorTest.php +++ b/tests/Bandits/BanditEvaluatorTest.php @@ -13,6 +13,8 @@ class BanditEvaluatorTest extends TestCase { + private BanditEvaluator $evaluator; + public function setUp(): void { $this->evaluator = new BanditEvaluator(); diff --git a/tests/EppoClientTest.php b/tests/EppoClientTest.php index 4e1b931..b668785 100644 --- a/tests/EppoClientTest.php +++ b/tests/EppoClientTest.php @@ -271,7 +271,7 @@ public function testRepoTestCases(): void $subject['subjectAttributes'], $test['defaultValue'] ); - $this->assertEquals($subject['assignment'], $result, "$testFile ${test['flag']}"); + $this->assertEquals($subject['assignment'], $result, "$testFile {$test['flag']}"); } } } diff --git a/tests/RuleEvaluatorTest.php b/tests/RuleEvaluatorTest.php index a4953dd..a036c01 100644 --- a/tests/RuleEvaluatorTest.php +++ b/tests/RuleEvaluatorTest.php @@ -45,6 +45,7 @@ final class RuleEvaluatorTest extends TestCase */ private array $nonMatchingSplits; private Rule $ruleWithPreciseMatchesCondition; + private Rule $ruleWithNotMatchesConditionCondition; /** * @param string|null $name