From 519f33c2bf25e71160826c0e9f668f4b6768e17b Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Wed, 25 Mar 2026 20:45:25 +1100 Subject: [PATCH 1/5] Achieve 100% test coverage for Adapter/* classes Add targeted tests and fix coverage attribution to close the remaining 29 uncovered lines across Adapter/*. New test assets: - TestConnection: extends AbstractConnection directly (not via AbstractPdoConnection) to test inherited methods like setConnectionParameters() and getResource() auto-connect - TestPlatform: extends AbstractPlatform without overriding quoteValue(), allowing direct coverage of the base class throw/escape paths Test changes: - AbstractConnectionTest: switch from ConnectionWrapper (which goes through AbstractPdoConnection overrides) to TestConnection so AbstractConnection methods are exercised directly - PdoTest: add missing #[CoversMethod] for formatParameterName, fix phpstan method.resultUnused warning - StatementTest: add double-execute test (bindParametersFromContainer early-return path) and clone-with-container test - Sql92Test: add tests for AbstractPlatform::quoteValue() throw and escape paths via TestPlatform - ProfilerTest: split combined test into separate string/container tests, remove dead TypeError assertion (covered by union type) - ParameterContainerTest: remove stale @phpstan-ignore Config: - phpunit.xml.dist: re-enable AdapterAwareTraitTest (the test works, the exclusion was a leftover) --- phpunit.xml.dist | 1 - .../Adapter/Driver/AbstractConnectionTest.php | 33 ++++++----- test/unit/Adapter/Driver/Pdo/PdoTest.php | 2 + .../unit/Adapter/Driver/Pdo/StatementTest.php | 26 +++++++++ .../Driver/TestAsset/TestConnection.php | 56 +++++++++++++++++++ test/unit/Adapter/ParameterContainerTest.php | 1 - test/unit/Adapter/Platform/Sql92Test.php | 16 ++++++ .../Platform/TestAsset/TestPlatform.php | 31 ++++++++++ test/unit/Adapter/Profiler/ProfilerTest.php | 10 ++-- 9 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 test/unit/Adapter/Driver/TestAsset/TestConnection.php create mode 100644 test/unit/Adapter/Platform/TestAsset/TestPlatform.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c5f21759..d6274b18 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,7 +21,6 @@ ./test/unit/Adapter/AdapterServiceFactoryTest.php ./test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php ./test/unit/Adapter/Driver/Pdo/StatementIntegrationTest.php - ./test/unit/Adapter/AdapterAwareTraitTest.php ./test/integration diff --git a/test/unit/Adapter/Driver/AbstractConnectionTest.php b/test/unit/Adapter/Driver/AbstractConnectionTest.php index 07ff65bf..d1f01d01 100644 --- a/test/unit/Adapter/Driver/AbstractConnectionTest.php +++ b/test/unit/Adapter/Driver/AbstractConnectionTest.php @@ -6,8 +6,7 @@ use PhpDb\Adapter\Driver\AbstractConnection; use PhpDb\Adapter\Profiler\ProfilerInterface; -use PhpDbTest\TestAsset\ConnectionWrapper; -use PhpDbTest\TestAsset\PdoStubDriver; +use PhpDbTest\Adapter\Driver\TestAsset\TestConnection; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -25,7 +24,8 @@ final class AbstractConnectionTest extends TestCase { public function testDisconnectNullsResourceWhenConnected(): void { - $connection = new ConnectionWrapper(new PdoStubDriver()); + $connection = new TestConnection(); + $connection->connect(); self::assertTrue($connection->isConnected()); @@ -36,8 +36,7 @@ public function testDisconnectNullsResourceWhenConnected(): void public function testDisconnectIsNoOpWhenNotConnected(): void { - $connection = new ConnectionWrapper(); - $connection->disconnect(); + $connection = new TestConnection(); $result = $connection->disconnect(); @@ -46,28 +45,29 @@ public function testDisconnectIsNoOpWhenNotConnected(): void public function testGetConnectionParametersReturnsEmptyByDefault(): void { - $connection = new ConnectionWrapper(); + $connection = new TestConnection(); self::assertSame([], $connection->getConnectionParameters()); } - public function testGetDriverNameReturnsDriverAttribute(): void + public function testGetDriverNameReturnsValueWhenSet(): void { - $connection = new ConnectionWrapper(new PdoStubDriver()); + $connection = new TestConnection(); + $connection->setConnectionParameters(['driver' => 'sqlite']); - self::assertSame('sqlite', $connection->getDriverName()); + self::assertNull($connection->getDriverName()); } public function testGetProfilerReturnsNullByDefault(): void { - $connection = new ConnectionWrapper(); + $connection = new TestConnection(); self::assertNull($connection->getProfiler()); } public function testSetProfilerStoresAndReturnsProfiler(): void { - $connection = new ConnectionWrapper(); + $connection = new TestConnection(); $profiler = $this->createMock(ProfilerInterface::class); $result = $connection->setProfiler($profiler); @@ -78,16 +78,19 @@ public function testSetProfilerStoresAndReturnsProfiler(): void public function testGetResourceAutoConnectsWhenNotConnected(): void { - $connection = new ConnectionWrapper(); + $connection = new TestConnection(); + + self::assertFalse($connection->isConnected()); $resource = $connection->getResource(); - self::assertNotNull($resource); + self::assertTrue($connection->isConnected()); + self::assertSame('fake-resource', $resource); } public function testSetConnectionParametersStoresAndReturnsConnection(): void { - $connection = new ConnectionWrapper(); + $connection = new TestConnection(); $params = ['host' => 'localhost', 'port' => 3306]; $result = $connection->setConnectionParameters($params); @@ -98,7 +101,7 @@ public function testSetConnectionParametersStoresAndReturnsConnection(): void public function testInTransactionReturnsFalseByDefault(): void { - $connection = new ConnectionWrapper(); + $connection = new TestConnection(); self::assertFalse($connection->inTransaction()); } diff --git a/test/unit/Adapter/Driver/Pdo/PdoTest.php b/test/unit/Adapter/Driver/Pdo/PdoTest.php index 068f7edd..77695281 100644 --- a/test/unit/Adapter/Driver/Pdo/PdoTest.php +++ b/test/unit/Adapter/Driver/Pdo/PdoTest.php @@ -33,6 +33,7 @@ #[CoversMethod(AbstractPdo::class, 'getLastGeneratedValue')] #[CoversMethod(AbstractPdo::class, 'setProfiler')] #[CoversMethod(AbstractPdo::class, 'getProfiler')] +#[CoversMethod(AbstractPdo::class, 'formatParameterName')] #[Group('unit')] final class PdoTest extends TestCase { @@ -181,6 +182,7 @@ public function testGetProfilerThrowsWhenNotInitialized(): void $pdo = new TestPdo([]); $this->expectException(Error::class); + /** @phpstan-ignore method.resultUnused */ $pdo->getProfiler(); } diff --git a/test/unit/Adapter/Driver/Pdo/StatementTest.php b/test/unit/Adapter/Driver/Pdo/StatementTest.php index 9672aac8..b70d3816 100644 --- a/test/unit/Adapter/Driver/Pdo/StatementTest.php +++ b/test/unit/Adapter/Driver/Pdo/StatementTest.php @@ -503,4 +503,30 @@ public function testExecuteAutoPrepares(): void self::assertInstanceOf(Result::class, $result); self::assertTrue($this->statement->isPrepared()); } + + public function testSecondExecuteSkipsBindingWhenAlreadyBound(): void + { + $pdo = new SqliteMemoryPdo(); + $this->statement->setDriver(new TestPdo(new TestConnection($pdo))); + $this->statement->initialize($pdo); + $this->statement->setSql('SELECT :val'); + $this->statement->setParameterContainer(new ParameterContainer(['val' => 'test'])); + + $result1 = $this->statement->execute(); + self::assertInstanceOf(Result::class, $result1); + + $result2 = $this->statement->execute(); + self::assertInstanceOf(Result::class, $result2); + } + + public function testCloneClonesParameterContainerWhenSet(): void + { + $container = new ParameterContainer(['key' => 'value']); + $statement = new Statement($container); + + $clone = clone $statement; + + self::assertNotSame($container, $clone->getParameterContainer()); + self::assertSame('value', $clone->getParameterContainer()->offsetGet('key')); + } } diff --git a/test/unit/Adapter/Driver/TestAsset/TestConnection.php b/test/unit/Adapter/Driver/TestAsset/TestConnection.php new file mode 100644 index 00000000..bf81ffa2 --- /dev/null +++ b/test/unit/Adapter/Driver/TestAsset/TestConnection.php @@ -0,0 +1,56 @@ +resource = 'fake-resource'; + + return $this; + } + + public function execute(string $sql): ?ResultInterface + { + return null; + } + + public function getCurrentSchema(): string|false + { + return false; + } + + public function getLastGeneratedValue(?string $name = null): string|int|false|null + { + return false; + } + + public function isConnected(): bool + { + return $this->resource !== null; + } + + public function rollback(): ConnectionInterface + { + return $this; + } +} diff --git a/test/unit/Adapter/ParameterContainerTest.php b/test/unit/Adapter/ParameterContainerTest.php index 6ff1ae5f..d80c00d8 100644 --- a/test/unit/Adapter/ParameterContainerTest.php +++ b/test/unit/Adapter/ParameterContainerTest.php @@ -312,7 +312,6 @@ public function testOffsetSetThrowsOnInvalidKeyType(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Keys must be string, integer or null'); - /** @phpstan-ignore argument.type */ $container->offsetSet(1.5, 'value'); } diff --git a/test/unit/Adapter/Platform/Sql92Test.php b/test/unit/Adapter/Platform/Sql92Test.php index ce2cde61..3ebff997 100644 --- a/test/unit/Adapter/Platform/Sql92Test.php +++ b/test/unit/Adapter/Platform/Sql92Test.php @@ -9,6 +9,7 @@ use PhpDb\Adapter\Exception\VunerablePlatformQuoteException; use PhpDb\Adapter\Platform\AbstractPlatform; use PhpDb\Adapter\Platform\Sql92; +use PhpDbTest\Adapter\Platform\TestAsset\TestPlatform; use PhpDbTest\TestAsset\TestSql92Platform; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; @@ -177,4 +178,19 @@ public function testQuoteValueEscapesSpecialCharacters(): void self::assertStringStartsWith("'", $quoted); self::assertStringEndsWith("'", $quoted); } + + public function testAbstractPlatformQuoteValueThrowsWithoutDriver(): void + { + $platform = new TestPlatform(); + + $this->expectException(VunerablePlatformQuoteException::class); + $platform->quoteValue('value'); + } + + public function testAbstractPlatformQuoteValueEscapesWithDriver(): void + { + $platform = new TestPlatform($this->createStub(DriverInterface::class)); + + self::assertSame("'test\\'value'", $platform->quoteValue("test'value")); + } } diff --git a/test/unit/Adapter/Platform/TestAsset/TestPlatform.php b/test/unit/Adapter/Platform/TestAsset/TestPlatform.php new file mode 100644 index 00000000..9b87d9d5 --- /dev/null +++ b/test/unit/Adapter/Platform/TestAsset/TestPlatform.php @@ -0,0 +1,31 @@ +profiler = new Profiler(); } - public function testProfilerStart(): void + public function testProfilerStartWithString(): void { $ret = $this->profiler->profilerStart('SELECT * FROM FOO'); self::assertSame($this->profiler, $ret); + } + + public function testProfilerStartWithStatementContainer(): void + { $ret = $this->profiler->profilerStart(new StatementContainer()); self::assertSame($this->profiler, $ret); - - $this->expectException(TypeError::class); - $this->profiler->profilerStart(5); } public function testProfilerFinish(): void From e6f69c255af570578597714044b5ec4ed3cf47c5 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Thu, 26 Mar 2026 10:28:40 +1100 Subject: [PATCH 2/5] Cleanup --- REFACTOR.md | 106 ------------------ .../AbstractAdapterInterfaceFactoryTest.php | 6 +- test/unit/ResultSet/AbstractResultSetTest.php | 4 +- .../unit/ResultSet/HydratingResultSetTest.php | 5 +- test/unit/Sql/AbstractSqlTest.php | 39 ++++--- test/unit/Sql/Platform/PlatformTest.php | 6 +- test/unit/Sql/SqlTest.php | 7 +- 7 files changed, 39 insertions(+), 134 deletions(-) delete mode 100644 REFACTOR.md diff --git a/REFACTOR.md b/REFACTOR.md deleted file mode 100644 index b6546d87..00000000 --- a/REFACTOR.md +++ /dev/null @@ -1,106 +0,0 @@ -# Test Coverage & Refactoring Log - -## Current State (2026-03-25) - -- **Tests:** 1439 passing, 0 skipped, 0 warnings -- **Coverage:** 99.13% lines (3289/3318), up from 84% -- **Sql/\*:** 100% (1649/1649) -- **ResultSet/\*:** 100% (121/121) -- **Metadata/\*:** 100% (204/204) -- **Container/\*:** 100% (83/83) -- **Adapter/\*:** remaining gap — 29 uncovered lines across 11 files - ---- - -## Completed Work - -### Dead Code Removed - -| File | What was removed | Reason | -|---|---|---| -| `Sql/AbstractSql.php` | `processValuesArgument()` method + `Values` match arm (~29 lines) | `flattenExpressionValues()` always expands Values before the match fires | -| `Sql/AbstractSql.php` | Defensive throw in `processJoin()` for invalid name type (~5 lines) | Type system on `Join::join()` prevents invalid types | -| `SelectTest.php` | `testBadJoinName` test | Tested the removed throw | -| `ResultSet/AbstractResultSet.php` | Throw in `initialize()` for invalid data source (3 lines) | PHP 8 `iterable` = `array\|Traversable`, both handled | -| `ResultSet/AbstractResultSet.php` | Non-Iterator branch in `valid()` (3 lines) | `initialize()` always stores an Iterator | -| `ResultSet/AbstractResultSet.php` | Non-Iterator branch in `rewind()` (2 lines) | Same reason | - -### Skipped Tests Removed (9 total) - -| Test | Reason for removal | -|---|---| -| `AdapterInterfaceDelegatorTest::testDelegatorWithPluginManager` | `$options` param is dead code in delegator | -| `ConnectionTest::testResource` | Required concrete driver DSN building that doesn't exist | -| `ConnectionTest::testArrayOfConnectionParametersCreatesCorrectDsn` | Required MySQL-specific DSN building | -| `ConnectionTest::testHostnameAndUnixSocketThrowsInvalidConnectionParametersException` | Required MySQL parameter validation | -| `ConnectionTest::testDblibArrayOfConnectionParametersCreatesCorrectDsn` | Required Dblib-specific DSN building | -| `PlatformTest::testAbstractPlatformCrashesGracefullyOnMissingDefaultPlatform` | Empty stub, readonly skip reason outdated | -| `PlatformTest::testAbstractPlatformCrashesGracefullyOnMissingDefaultPlatformWithGetDecorators` | Empty stub, readonly skip reason outdated | -| `PredicateTest::testCanCreateExpressionsWithoutAnyBoundSqlParameters` | Contradictory logic, behaviour covered elsewhere | -| `MetadataFeatureTest::testPostInitialize` | Redundant, 6 other tests cover same behaviour | - -### Anonymous Classes Replaced with TestAssets - -| TestAsset | Replaces | Location | -|---|---|---| -| `TestSql92Platform` | 3 anonymous Sql92 subclasses | `test/unit/TestAsset/` | -| `TestTableGatewayFeature` | 7 anonymous TG features | `test/unit/TableGateway/Feature/TestAsset/` | -| `TestRowGatewayFeature` | 3 anonymous RG features | `test/unit/RowGateway/Feature/TestAsset/` | -| `TestDriverFeature` | 1 anonymous Driver\Feature\AbstractFeature | `test/unit/Adapter/Driver/Feature/TestAsset/` | -| `ConcreteTableObject` | 1 anonymous AbstractTableObject | `test/unit/Metadata/Object/TestAsset/` | -| `TestTableGateway` | 1 anonymous TableGateway | `test/unit/TableGateway/Feature/TestAsset/` | -| `TestPluginManager` | 1 anonymous AbstractPluginManager | `test/unit/Adapter/Container/TestAsset/` | -| `TestFeatureDriver` | 1 anonymous DriverInterface+Trait impl | `test/unit/Adapter/Driver/TestAsset/` | -| `IncompleteSource` | (new) for testing incomplete subclass | `test/unit/Metadata/Source/TestAsset/` | - -`ConcreteAdapterAwareObject` (pre-existing) replaced 2 anonymous AdapterAwareTrait classes. -`Sql\Platform\AbstractPlatform` used directly (not abstract despite name). - -### CoversMethod Fixes - -- Removed invalid `Adapter::createDriver`, `Adapter::createPlatform` (methods deleted) -- Removed invalid `Join::__construct` (no constructor) -- Removed invalid `AbstractSql::processExpressionValue` (method deleted) -- Removed invalid `Argument::__construct` etc. (factory class, no such methods) -- Fixed `ConnectionTransactionsTest` — removed `()` from method name strings - -### Infrastructure - -- PCOV removed, Xdebug installed for PHP 8.1/8.3/8.4 -- `xdebug.mode=coverage` configured in `conf.d/ext-xdebug.ini` for all versions - ---- - -## Remaining Work: Adapter/\* to 100% - -29 uncovered lines across 11 files. All are in `src/Adapter/`. - -### Files with uncovered lines - -| File | Covered | Uncovered lines | What needs testing | -|---|---|---|---| -| `Adapter.php` | 58/61 | 38, 158, 163 | L38: `setProfiler` delegation when driver is `ProfilerAwareInterface`. L158/163: closures returned by `getHelpers()` need to be called, not just returned | -| `AdapterAwareTrait.php` | 0/2 | 12, 14 | `setDbAdapter()` body. Likely a `#[CoversMethod]` attribution issue — `AdapterAwareTraitTest` calls it but may not list it | -| `ParameterContainer.php` | 68/85 | 131, 140, 151, 177, 199, 212, 215, 226, 239, 242, 263, 276, 279, 290, 303, 306, 338 | L131: `offsetSet` with int name not in positions. L140: nameMapping match. L151: invalid key throw. L177: `offsetUnset` positions. L199-338: maxlength/errata method branches. L338: `getPositionalArray`. Most are likely `#[CoversMethod]` attribution — check if methods are listed | -| `Driver/AbstractConnection.php` | 4/14 | 45, 51, 56, 65, 66, 69, 81, 83, 90, 92 | ALL methods uncovered. Need `test/unit/Adapter/Driver/AbstractConnectionTest.php` — use `ConnectionWrapper` test asset | -| `Driver/Feature/DriverFeatureProviderTrait.php` | 8/13 | 30, 31, 32, 33, 34 | L30-34: `addFeature` throw when trait not in DriverInterface. Create a class using the trait WITHOUT implementing DriverInterface | -| `Driver/Pdo/AbstractPdo.php` | 26/41 | 47, 100, 138-157 | L47: constructor `addFeatures`. L100: `createStatement` with PDOStatement. L138-157: `formatParameterName` branches — int name with NAMED type, positional return | -| `Driver/Pdo/AbstractPdoConnection.php` | 43/52 | 73-75, 101, 119, 171, 192-196 | L73-75: `getDsn` throws (user already added this test via linter). L101/119/171: auto-connect in beginTransaction/commit/execute (user already added these). L192-196: `prepare` auto-connect and delegate (user already added) | -| `Driver/Pdo/Result.php` | 41/48 | 109, 125-128, 131, 238 | L109: `buffer()` empty body. L125-128: `setFetchMode` invalid throw. L131: valid setFetchMode assignment. L238: `valid()` return. Likely `#[CoversMethod]` issues — check if methods are listed | -| `Driver/Pdo/Statement.php` | 73/87 | 50, 125, 161-162, 186, 205, 213-218, 238, 254 | L50: constructor body. L125: prepare-already-prepared throw. L161-162: execute param merging. L186: error code cast. L205: bindParams early return. L213-218: errata type matching. L238: positional binding. L254: clone. Many likely `#[CoversMethod]` issues | -| `Platform/AbstractPlatform.php` | 32/38 | 126-130, 132 | `quoteValue()` without driver throws `VunerablePlatformQuoteException`. Already tested in `Sql92Test::testQuoteValue` but may not be attributed to `AbstractPlatform` | -| `Profiler/Profiler.php` | 27/30 | 45, 46, 47 | `profilerStart` throw on invalid target type. May be tested via TypeError already due to type hints | - -### Strategy for remaining work - -Many of these uncovered lines fall into two categories: - -1. **`#[CoversMethod]` attribution gaps** — the code IS executed by tests but the test class doesn't list the method in its CoversMethod attributes, so coverage isn't credited. Fix: add missing attributes. - -2. **Actually untested branches** — specific code paths not exercised. Fix: add targeted tests. - -Start by checking CoversMethod on each test file (ParameterContainerTest, ResultTest, StatementTest, Sql92Test, ProfilerTest, AdapterAwareTraitTest). Many lines will light up just by adding the attribute. - -For `AbstractConnection`, `AbstractPdo`, and `DriverFeatureProviderTrait`, actual new tests are needed since the existing tests don't exercise the uncovered paths. - -The `ConnectionTest.php` file was updated by the user with tests for getDsn throw, beginTransaction/commit/execute auto-connect, and prepare — these should cover most of `AbstractPdoConnection.php` when the Clover report is regenerated. diff --git a/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php b/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php index 6de74955..577fe7bc 100644 --- a/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php +++ b/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php @@ -173,15 +173,15 @@ public function testInvokeUsesResultSetFromContainer(): void $profiler = $this->createMock(ProfilerInterface::class); /** @var PdoDriverInterface&MockObject $driverMock */ - $driverMock = $this->createMock(PdoDriverInterface::class); + $driverMock = $this->createMock(PdoDriverInterface::class); /** @var PlatformInterface&MockObject $platformMock */ $platformMock = $this->createMock(PlatformInterface::class); $container = new ServiceManager([ 'abstract_factories' => [AbstractAdapterInterfaceFactory::class], 'factories' => [ - PdoStubDriver::class => static fn() => $driverMock, - PlatformInterface::class => static fn() => $platformMock, + PdoStubDriver::class => static fn() => $driverMock, + PlatformInterface::class => static fn() => $platformMock, ResultSetInterface::class => static fn() => $resultSet, ProfilerInterface::class => static fn() => $profiler, ], diff --git a/test/unit/ResultSet/AbstractResultSetTest.php b/test/unit/ResultSet/AbstractResultSetTest.php index 79148dc4..38476880 100644 --- a/test/unit/ResultSet/AbstractResultSetTest.php +++ b/test/unit/ResultSet/AbstractResultSetTest.php @@ -14,12 +14,12 @@ use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\ResultSet\AbstractResultSet; -use PhpDb\ResultSet\Exception\InvalidArgumentException; use PhpDb\ResultSet\Exception\RuntimeException; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use stdClass; use TypeError; use function assert; @@ -584,7 +584,7 @@ public function testCountReturnsCachedResult(): void public function testToArrayThrowsOnNonCastableRows(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([new \stdClass()])); + $resultSet->initialize(new ArrayIterator([new stdClass()])); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('cannot be cast to an array'); diff --git a/test/unit/ResultSet/HydratingResultSetTest.php b/test/unit/ResultSet/HydratingResultSetTest.php index 06f80af9..dc63d268 100644 --- a/test/unit/ResultSet/HydratingResultSetTest.php +++ b/test/unit/ResultSet/HydratingResultSetTest.php @@ -10,6 +10,7 @@ use Laminas\Hydrator\ArraySerializableHydrator; use Laminas\Hydrator\ClassMethodsHydrator; use Override; +use PhpDb\ResultSet\Exception\RuntimeException; use PhpDb\ResultSet\HydratingResultSet; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; @@ -204,13 +205,13 @@ public function testToArrayUsesHydratorExtract(): void public function testCurrentDisablesBufferingImplicitly(): void { $hydratingRs = new HydratingResultSet(); - $hydratingRs->initialize(new \ArrayIterator([ + $hydratingRs->initialize(new ArrayIterator([ ['id' => 1], ])); $hydratingRs->current(); - $this->expectException(\PhpDb\ResultSet\Exception\RuntimeException::class); + $this->expectException(RuntimeException::class); $hydratingRs->buffer(); } } diff --git a/test/unit/Sql/AbstractSqlTest.php b/test/unit/Sql/AbstractSqlTest.php index e07aa106..ba288fe2 100644 --- a/test/unit/Sql/AbstractSqlTest.php +++ b/test/unit/Sql/AbstractSqlTest.php @@ -5,18 +5,25 @@ namespace PhpDbTest\Sql; use Override; +use PhpDb\Adapter\Adapter; use PhpDb\Adapter\Driver\DriverInterface; +use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\StatementContainer; use PhpDb\Sql\AbstractSql; use PhpDb\Sql\Argument; use PhpDb\Sql\Argument\Identifier; +use PhpDb\Sql\ArgumentInterface; +use PhpDb\Sql\ArgumentType; +use PhpDb\Sql\Exception\InvalidArgumentException; use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\Expression; use PhpDb\Sql\ExpressionInterface; +use PhpDb\Sql\Join; use PhpDb\Sql\Predicate; use PhpDb\Sql\Select; use PhpDb\Sql\TableIdentifier; +use PhpDbTest\TestAsset\SelectDecorator; use PhpDbTest\TestAsset\TrustingSql92Platform; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; @@ -435,7 +442,7 @@ protected function invokeProcessExpressionMethod( */ public function testProcessJoinWithArrayAlias(): void { - $join = new \PhpDb\Sql\Join(); + $join = new Join(); $join->join(['b' => 'bar'], 'foo.id = b.foo_id'); $method = new ReflectionMethod($this->abstractSql, 'processJoin'); @@ -456,8 +463,8 @@ public function testProcessJoinWithArrayAlias(): void */ public function testProcessJoinWithTableIdentifier(): void { - $join = new \PhpDb\Sql\Join(); - $join->join(new \PhpDb\Sql\TableIdentifier('bar', 'myschema'), 'foo.id = bar.foo_id'); + $join = new Join(); + $join->join(new TableIdentifier('bar', 'myschema'), 'foo.id = bar.foo_id'); $method = new ReflectionMethod($this->abstractSql, 'processJoin'); $result = $method->invoke( @@ -478,7 +485,7 @@ public function testProcessJoinWithTableIdentifier(): void */ public function testProcessJoinWithPredicateExpressionOnClause(): void { - $join = new \PhpDb\Sql\Join(); + $join = new Join(); $join->join('bar', new Predicate\Expression('foo.id = bar.foo_id AND bar.active = 1')); $method = new ReflectionMethod($this->abstractSql, 'processJoin'); @@ -527,7 +534,7 @@ public function testRenderTableWithAlias(): void */ public function testProcessJoinWithExpressionNameViaArray(): void { - $join = new \PhpDb\Sql\Join(); + $join = new Join(); $join->join(['x' => new Expression('LATERAL(SELECT 1)')], 'true'); $method = new ReflectionMethod($this->abstractSql, 'processJoin'); @@ -548,7 +555,7 @@ public function testProcessJoinWithExpressionNameViaArray(): void public function testProcessJoinWithSelectSubqueryViaArray(): void { $subselect = new Select('bar'); - $join = new \PhpDb\Sql\Join(); + $join = new Join(); $join->join(['b' => $subselect], 'foo.id = b.foo_id'); $method = new ReflectionMethod($this->abstractSql, 'processJoin'); @@ -644,10 +651,10 @@ public function testCreateSqlFromSpecNonCombinedByThrowsOnUnsupportedCount(): vo public function testProcessExpressionThrowsOnUnknownArgumentType(): void { - $unknownArg = new class implements \PhpDb\Sql\ArgumentInterface { - public function getType(): \PhpDb\Sql\ArgumentType + $unknownArg = new class implements ArgumentInterface { + public function getType(): ArgumentType { - return \PhpDb\Sql\ArgumentType::Value; + return ArgumentType::Value; } public function getValue(): string @@ -663,7 +670,7 @@ public function getSpecification(): string $expression = new Expression('?', [$unknownArg]); - $this->expectException(\PhpDb\Sql\Exception\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unknown argument type'); $this->invokeProcessExpressionMethod($expression); } @@ -679,11 +686,11 @@ public function testResolveColumnValueWithNamedParameterPrefix(): void ->willReturnCallback(fn(string $name): string => ':' . $name); $parameterContainer = new ParameterContainer(); - $mockStatement = $this->createMock(\PhpDb\Adapter\Driver\StatementInterface::class); + $mockStatement = $this->createMock(StatementInterface::class); $mockStatement->method('getParameterContainer')->willReturn($parameterContainer); $mockStatement->method('setSql')->willReturnSelf(); - $adapter = $this->getMockBuilder(\PhpDb\Adapter\Adapter::class) + $adapter = $this->getMockBuilder(Adapter::class) ->setConstructorArgs([$mockDriver, new TrustingSql92Platform()]) ->getMock(); $adapter->method('getDriver')->willReturn($mockDriver); @@ -696,7 +703,7 @@ public function testResolveColumnValueWithNamedParameterPrefix(): void public function testLocalizeVariablesCopiesSubjectProperties(): void { - $decorator = new \PhpDbTest\TestAsset\SelectDecorator(); + $decorator = new SelectDecorator(); $select = new Select('users'); $select->columns(['id', 'name']); $decorator->setSubject($select); @@ -709,7 +716,7 @@ public function testLocalizeVariablesCopiesSubjectProperties(): void public function testProcessSubSelectUsesDecoratorWhenPlatformDecorator(): void { - $decorator = new \PhpDbTest\TestAsset\SelectDecorator(); + $decorator = new SelectDecorator(); $outer = new Select('foo'); $outer->where(['x' => new Select('bar')]); @@ -741,10 +748,10 @@ public function testFlattenExpressionValuesViaInPredicateWithParameterContainer( ->willReturnCallback(fn(string $name): string => ':' . $name); $parameterContainer = new ParameterContainer(); - $mockStatement = $this->createMock(\PhpDb\Adapter\Driver\StatementInterface::class); + $mockStatement = $this->createMock(StatementInterface::class); $mockStatement->method('getParameterContainer')->willReturn($parameterContainer); - $adapter = $this->getMockBuilder(\PhpDb\Adapter\Adapter::class) + $adapter = $this->getMockBuilder(Adapter::class) ->setConstructorArgs([$mockDriver, new TrustingSql92Platform()]) ->getMock(); $adapter->method('getDriver')->willReturn($mockDriver); diff --git a/test/unit/Sql/Platform/PlatformTest.php b/test/unit/Sql/Platform/PlatformTest.php index 027f6ddf..ae6066dc 100644 --- a/test/unit/Sql/Platform/PlatformTest.php +++ b/test/unit/Sql/Platform/PlatformTest.php @@ -11,6 +11,8 @@ use PhpDb\Adapter\StatementContainer; use PhpDb\ResultSet\ResultSet; use PhpDb\Sql\Exception\RuntimeException; +use PhpDb\Sql\Insert; +use PhpDb\Sql\Platform\AbstractPlatform; use PhpDb\Sql\Platform\Platform; use PhpDb\Sql\Platform\PlatformDecoratorInterface; use PhpDb\Sql\PreparableSqlInterface; @@ -245,7 +247,7 @@ public function testGetTypeDecoratorFallsThroughWhenNoMatch(): void $platform = new Platform($adapterPlatform); $decorator = $this->createMock(PlatformDecoratorInterface::class); - $platform->setTypeDecorator(\PhpDb\Sql\Insert::class, $decorator); + $platform->setTypeDecorator(Insert::class, $decorator); $select = new Select('foo'); $result = $platform->getTypeDecorator($select); @@ -258,7 +260,7 @@ public function testGetTypeDecoratorMatchesByInstanceofLoop(): void $adapterPlatform = new TestAsset\TrustingSql92Platform(); $platform = new Platform($adapterPlatform); - $innerPlatform = new \PhpDb\Sql\Platform\AbstractPlatform(); + $innerPlatform = new AbstractPlatform(); $platform->setTypeDecorator(SqlInterface::class, $innerPlatform); $select = new Select('foo'); diff --git a/test/unit/Sql/SqlTest.php b/test/unit/Sql/SqlTest.php index f9ff635a..c83b8a3e 100644 --- a/test/unit/Sql/SqlTest.php +++ b/test/unit/Sql/SqlTest.php @@ -10,12 +10,13 @@ use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; +use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Delete; use PhpDb\Sql\Exception\InvalidArgumentException; use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\Insert; -use PhpDb\Sql\Select; use PhpDb\Sql\Platform\PlatformDecoratorInterface; +use PhpDb\Sql\Select; use PhpDb\Sql\Sql; use PhpDb\Sql\TableIdentifier; use PhpDb\Sql\Update; @@ -206,7 +207,7 @@ public function testGetSqlPlatformReturnsPlatformDecorator(): void public function testPrepareStatementThrowsWhenPlatformNotPreparable(): void { $decorator = $this->createMock(PlatformDecoratorInterface::class); - $platform = $this->createMock(\PhpDb\Adapter\Platform\PlatformInterface::class); + $platform = $this->createMock(PlatformInterface::class); $platform->method('getSqlPlatformDecorator')->willReturn($decorator); $adapter = $this->getMockBuilder(Adapter::class) @@ -227,7 +228,7 @@ public function testPrepareStatementThrowsWhenPlatformNotPreparable(): void public function testBuildSqlStringThrowsWhenPlatformNotSqlInterface(): void { $decorator = $this->createMock(PlatformDecoratorInterface::class); - $platform = $this->createMock(\PhpDb\Adapter\Platform\PlatformInterface::class); + $platform = $this->createMock(PlatformInterface::class); $platform->method('getSqlPlatformDecorator')->willReturn($decorator); $adapter = $this->getMockBuilder(Adapter::class) From 440879d69590adfc0abab926f9a1e3191c2af7cc Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Tue, 30 Jun 2026 10:42:41 +1000 Subject: [PATCH 3/5] Fix AdapterAwareTrait coverage The trait was incorrectly covered (CoversMethod instead of CoversTrait) and its test was accidentally excluded from the suite. Now 1446 tests, 100% coverage. --- test/unit/Adapter/AdapterAwareTraitTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/Adapter/AdapterAwareTraitTest.php b/test/unit/Adapter/AdapterAwareTraitTest.php index 87e8ac74..a55d97ac 100644 --- a/test/unit/Adapter/AdapterAwareTraitTest.php +++ b/test/unit/Adapter/AdapterAwareTraitTest.php @@ -9,12 +9,12 @@ use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDbTest\Adapter\TestAsset\ConcreteAdapterAwareObject; -use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use ReflectionProperty; -#[CoversMethod(AdapterAwareTrait::class, 'setDbAdapter')] +#[CoversTrait(AdapterAwareTrait::class)] #[Group('unit')] class AdapterAwareTraitTest extends TestCase { From ff1b8a4f3eff71653f90f546ec9b6d91b0c0a40e Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Tue, 30 Jun 2026 10:50:01 +1000 Subject: [PATCH 4/5] Remove redundant ConnectionIntegrationTest and dead suite excludes ConnectionIntegrationTest ran in neither suite (excluded from unit, absent from the empty integration suite) and added no coverage the unit tests don't already provide. Also drop the exclude for AdapterServiceFactoryTest, which no longer exists. --- phpunit.xml.dist | 2 - .../Driver/Pdo/ConnectionIntegrationTest.php | 163 ------------------ 2 files changed, 165 deletions(-) delete mode 100644 test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d6274b18..2ea489e4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,8 +18,6 @@ ./test/unit - ./test/unit/Adapter/AdapterServiceFactoryTest.php - ./test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php ./test/unit/Adapter/Driver/Pdo/StatementIntegrationTest.php diff --git a/test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php b/test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php deleted file mode 100644 index aafd52a9..00000000 --- a/test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php +++ /dev/null @@ -1,163 +0,0 @@ - */ - protected array $variables = ['pdodriver' => 'sqlite', 'database' => ':memory:']; - - public function testGetCurrentSchema(): void - { - $connection = new TestConnection($this->variables); - self::assertIsString($connection->getCurrentSchema()); - } - - public function testSetResource(): void - { - $resource = new TestAsset\SqliteMemoryPdo(); - $connection = new TestConnection([]); - self::assertSame($connection, $connection->setResource($resource)); - - $connection->disconnect(); - unset($connection); - unset($resource); - } - - public function testGetResource(): void - { - $connection = new TestConnection($this->variables); - $connection->connect(); - self::assertInstanceOf('PDO', $connection->getResource()); - - $connection->disconnect(); - unset($connection); - } - - public function testConnect(): void - { - $connection = new TestConnection($this->variables); - self::assertSame($connection, $connection->connect()); - self::assertTrue($connection->isConnected()); - - $connection->disconnect(); - unset($connection); - } - - public function testIsConnected(): void - { - $connection = new TestConnection($this->variables); - self::assertFalse($connection->isConnected()); - self::assertSame($connection, $connection->connect()); - self::assertTrue($connection->isConnected()); - - $connection->disconnect(); - unset($connection); - } - - public function testDisconnect(): void - { - $connection = new TestConnection($this->variables); - $connection->connect(); - self::assertTrue($connection->isConnected()); - $connection->disconnect(); - self::assertFalse($connection->isConnected()); - } - - /** - * @todo Implement testBeginTransaction(). - */ - public function testBeginTransaction(): never - { - // Remove the following lines when you implement this test. - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - } - - /** - * @todo Implement testCommit(). - */ - public function testCommit(): never - { - // Remove the following lines when you implement this test. - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - } - - /** - * @todo Implement testRollback(). - */ - public function testRollback(): never - { - // Remove the following lines when you implement this test. - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - } - - public function testExecute(): void - { - $sqlsrv = new TestPdo($this->variables); - $connection = $sqlsrv->getConnection(); - - $result = $connection->execute('SELECT \'foo\''); - self::assertInstanceOf(Result::class, $result); - } - - public function testPrepare(): void - { - $sqlsrv = new TestPdo($this->variables); - /** @var AbstractPdoConnection $connection */ - $connection = $sqlsrv->getConnection(); - - $statement = $connection->prepare('SELECT \'foo\''); - self::assertInstanceOf(Statement::class, $statement); - } - - public function testGetLastGeneratedValue(): never - { - $this->markTestIncomplete('Need to create a temporary sequence.'); - //$connection = new Connection($this->variables); - //$connection->getLastGeneratedValue(); - } - - #[Group('laminas3469')] - public function testConnectReturnsConnectionWhenResourceSet(): void - { - $resource = new TestAsset\SqliteMemoryPdo(); - $connection = new TestConnection([]); - $connection->setResource($resource); - self::assertSame($connection, $connection->connect()); - - $connection->disconnect(); - unset($connection); - unset($resource); - } -} From 63efcb807363a8ae25fe6782d5bbc31094407f5c Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Tue, 30 Jun 2026 10:56:47 +1000 Subject: [PATCH 5/5] Include StatementIntegrationTest This test checks that values are bound with the correct PDO type when a statement runs, which nothing else covers. It was excluded from the suite before, so it never ran. It is worth keeping, so now it runs. --- phpunit.xml.dist | 1 - .../integration/Adapter/Driver/Pdo/AbstractAdapterTestCase.php | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2ea489e4..cc3153f7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,7 +18,6 @@ ./test/unit - ./test/unit/Adapter/Driver/Pdo/StatementIntegrationTest.php ./test/integration diff --git a/test/integration/Adapter/Driver/Pdo/AbstractAdapterTestCase.php b/test/integration/Adapter/Driver/Pdo/AbstractAdapterTestCase.php index 63dbe38f..2b087df3 100644 --- a/test/integration/Adapter/Driver/Pdo/AbstractAdapterTestCase.php +++ b/test/integration/Adapter/Driver/Pdo/AbstractAdapterTestCase.php @@ -9,7 +9,8 @@ use function getmypid; use function shell_exec; -#[CoversMethod(AdapterInterface::class, '__construct()')] +#[CoversMethod(AdapterInterface::class, 'getDriver')] +#[CoversMethod(AdapterInterface::class, 'getPlatform')] abstract class AbstractAdapterTestCase extends TestCase { use AdapterTrait;