diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ade140..c3cc642e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Fix missing instrumentation of the `Statement::execute()` method of Doctrine DBAL (#548) + ## 4.2.1 (2021-08-24) - Fix return type for `TracingDriver::getDatabase()` method (#541) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9ee975a5..1e060e0c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -95,31 +95,6 @@ parameters: count: 1 path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterControllerEvent not found\\.$#" - count: 1 - path: src/aliases.php - - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterResponseEvent not found\\.$#" - count: 1 - path: src/aliases.php - - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent not found\\.$#" - count: 2 - path: src/aliases.php - - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseForExceptionEvent not found\\.$#" - count: 1 - path: src/aliases.php - - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\PostResponseEvent not found\\.$#" - count: 1 - path: src/aliases.php - - message: "#^Class Symfony\\\\Bundle\\\\FrameworkBundle\\\\Client not found\\.$#" count: 1 @@ -280,3 +255,48 @@ parameters: count: 1 path: tests/Tracing/Doctrine/DBAL/TracingDriverTest.php + - + message: "#^Trying to mock an undefined method closeCursor\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method columnCount\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method errorCode\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method errorInfo\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method fetch\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method fetchAll\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method fetchColumn\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method rowCount\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + + - + message: "#^Trying to mock an undefined method setFetchMode\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + diff --git a/phpstan.neon b/phpstan.neon index b04275a4..ff77da37 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,11 +6,16 @@ parameters: paths: - src - tests - excludes_analyse: + bootstrapFiles: + - src/aliases.php + excludePaths: + - src/aliases.php + - src/Tracing/Doctrine/DBAL/TracingStatementForV2.php - tests/End2End/App dynamicConstantNames: - Symfony\Component\HttpKernel\Kernel::VERSION - Symfony\Component\HttpKernel\Kernel::VERSION_ID - - Doctrine\DBAL\Version::VERSION stubFiles: - tests/Stubs/Profile.phpstub + featureToggles: + disableRuntimeReflectionProvider: true diff --git a/psalm-baseline.xml b/psalm-baseline.xml index f7d837b6..fa32acb6 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + ConsoleListener @@ -8,4 +8,38 @@ public function __construct(HubInterface $hub, bool $captureErrors = true) + + + $this->decoratedStatement + $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params) + + + \Traversable + bool + + + TracingStatementForV2 + + + closeCursor + columnCount + errorCode + errorInfo + fetch + fetchAll + fetchColumn + rowCount + setFetchMode + + + + + FilterControllerEvent + FilterResponseEvent + GetResponseEvent + GetResponseEvent + GetResponseForExceptionEvent + PostResponseEvent + + diff --git a/src/DependencyInjection/Compiler/DbalTracingPass.php b/src/DependencyInjection/Compiler/DbalTracingPass.php index 28d9b86c..d1e4d37f 100644 --- a/src/DependencyInjection/Compiler/DbalTracingPass.php +++ b/src/DependencyInjection/Compiler/DbalTracingPass.php @@ -4,7 +4,6 @@ namespace Sentry\SentryBundle\DependencyInjection\Compiler; -use Doctrine\DBAL\Driver\ResultStatement; use Doctrine\DBAL\Result; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware; @@ -57,7 +56,7 @@ public function process(ContainerBuilder $container): void throw new \InvalidArgumentException(sprintf('The Doctrine connection "%s" does not exists and cannot be instrumented.', $connectionName)); } - if (!interface_exists(ResultStatement::class)) { + if (class_exists(Result::class)) { $this->configureConnectionForDoctrineDBALVersion3($container, $connectionName); } else { $this->configureConnectionForDoctrineDBALVersion2($container, $connectionName); diff --git a/src/Tracing/Doctrine/DBAL/AbstractTracingStatement.php b/src/Tracing/Doctrine/DBAL/AbstractTracingStatement.php new file mode 100644 index 00000000..ba489196 --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/AbstractTracingStatement.php @@ -0,0 +1,84 @@ + The span tags + */ + protected $spanTags; + + /** + * Constructor. + * + * @param HubInterface $hub The current hub + * @param Statement $decoratedStatement The decorated statement + * @param string $sqlQuery The SQL query executed by the decorated statement + * @param array $spanTags The span tags + */ + public function __construct(HubInterface $hub, Statement $decoratedStatement, string $sqlQuery, array $spanTags) + { + $this->hub = $hub; + $this->decoratedStatement = $decoratedStatement; + $this->sqlQuery = $sqlQuery; + $this->spanTags = $spanTags; + } + + /** + * Calls the given callback by passing to it the specified arguments and + * wrapping its execution into a child {@see Span} of the current one. + * + * @param callable $callback The function to call + * @param mixed ...$args The arguments to pass to the callback + * + * @phpstan-template T + * + * @phpstan-param callable(mixed...): T $callback + * + * @phpstan-return T + */ + protected function traceFunction(SpanContext $spanContext, callable $callback, ...$args) + { + $span = $this->hub->getSpan(); + + if (null !== $span) { + $span = $span->startChild($spanContext); + } + + try { + return $callback(...$args); + } finally { + if (null !== $span) { + $span->finish(); + } + } + } +} diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverConnection.php b/src/Tracing/Doctrine/DBAL/TracingDriverConnection.php index 0424ff50..c4bd3a80 100644 --- a/src/Tracing/Doctrine/DBAL/TracingDriverConnection.php +++ b/src/Tracing/Doctrine/DBAL/TracingDriverConnection.php @@ -58,11 +58,6 @@ final class TracingDriverConnection implements DriverConnectionInterface */ private $decoratedConnection; - /** - * @var string The name of the database platform - */ - private $databasePlatform; - /** * @var array The tags to attach to the span */ @@ -84,8 +79,7 @@ public function __construct( ) { $this->hub = $hub; $this->decoratedConnection = $decoratedConnection; - $this->databasePlatform = $databasePlatform; - $this->spanTags = $this->getSpanTags($params); + $this->spanTags = $this->getSpanTags($databasePlatform, $params); } /** @@ -93,9 +87,11 @@ public function __construct( */ public function prepare($sql): Statement { - return $this->traceFunction(self::SPAN_OP_CONN_PREPARE, $sql, function () use ($sql): Statement { + $statement = $this->traceFunction(self::SPAN_OP_CONN_PREPARE, $sql, function () use ($sql): Statement { return $this->decoratedConnection->prepare($sql); }); + + return new TracingStatement($this->hub, $statement, $sql, $this->spanTags); } /** @@ -225,15 +221,16 @@ private function traceFunction(string $spanOperation, string $spanDescription, \ /** * Gets a map of key-value pairs that will be set as tags of the span. * - * @param array $params The connection params + * @param string $databasePlatform The database platform + * @param array $params The connection params * * @return array * * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md */ - private function getSpanTags(array $params): array + private function getSpanTags(string $databasePlatform, array $params): array { - $tags = ['db.system' => $this->databasePlatform]; + $tags = ['db.system' => $databasePlatform]; if (isset($params['user'])) { $tags['db.user'] = $params['user']; diff --git a/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php b/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php new file mode 100644 index 00000000..ffaaf7cb --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php @@ -0,0 +1,124 @@ + + */ +final class TracingStatementForV2 extends AbstractTracingStatement implements \IteratorAggregate, Statement +{ + /** + * {@inheritdoc} + */ + public function getIterator(): \Traversable + { + return $this->decoratedStatement; + } + + /** + * {@inheritdoc} + */ + public function closeCursor(): bool + { + return $this->decoratedStatement->closeCursor(); + } + + /** + * {@inheritdoc} + */ + public function columnCount(): int + { + return $this->decoratedStatement->columnCount(); + } + + /** + * {@inheritdoc} + */ + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null): bool + { + return $this->decoratedStatement->setFetchMode($fetchMode, $arg2, $arg3); + } + + /** + * {@inheritdoc} + */ + public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0) + { + return $this->decoratedStatement->fetch($fetchMode, $cursorOrientation, $cursorOffset); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) + { + return $this->decoratedStatement->fetchAll($fetchMode, $fetchArgument, $ctorArgs); + } + + /** + * {@inheritdoc} + */ + public function fetchColumn($columnIndex = 0) + { + return $this->decoratedStatement->fetchColumn($columnIndex); + } + + /** + * {@inheritdoc} + */ + public function errorCode() + { + return $this->decoratedStatement->errorCode(); + } + + /** + * {@inheritdoc} + */ + public function errorInfo(): array + { + return $this->decoratedStatement->errorInfo(); + } + + /** + * {@inheritdoc} + */ + public function rowCount(): int + { + return $this->decoratedStatement->rowCount(); + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + return $this->decoratedStatement->bindValue($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + return $this->decoratedStatement->bindParam($param, $variable, $type, $length); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null): bool + { + $spanContext = new SpanContext(); + $spanContext->setOp(self::SPAN_OP_STMT_EXECUTE); + $spanContext->setDescription($this->sqlQuery); + $spanContext->setTags($this->spanTags); + + return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params); + } +} diff --git a/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php b/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php new file mode 100644 index 00000000..fdc78715 --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php @@ -0,0 +1,42 @@ +decoratedStatement->bindValue($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + return $this->decoratedStatement->bindParam($param, $variable, $type, $length); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null): Result + { + $spanContext = new SpanContext(); + $spanContext->setOp(self::SPAN_OP_STMT_EXECUTE); + $spanContext->setDescription($this->sqlQuery); + $spanContext->setTags($this->spanTags); + + return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params); + } +} diff --git a/src/aliases.php b/src/aliases.php index fb37f890..f8e6bc18 100644 --- a/src/aliases.php +++ b/src/aliases.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\ExceptionConverterDriver as LegacyExceptionConverterDriverInterface; use Doctrine\DBAL\Driver\Middleware as DoctrineMiddlewareInterface; +use Doctrine\DBAL\Result; use Sentry\SentryBundle\EventListener\ErrorListenerExceptionEvent; use Sentry\SentryBundle\EventListener\RequestListenerControllerEvent; use Sentry\SentryBundle\EventListener\RequestListenerRequestEvent; @@ -14,6 +15,8 @@ use Sentry\SentryBundle\EventListener\SubRequestListenerRequestEvent; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\Compatibility\ExceptionConverterDriverInterface; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\Compatibility\MiddlewareInterface; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatementForV2; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatementForV3; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; @@ -28,72 +31,64 @@ if (version_compare(Kernel::VERSION, '4.3.0', '>=')) { if (!class_exists(ErrorListenerExceptionEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(ExceptionEvent::class, ErrorListenerExceptionEvent::class); } if (!class_exists(RequestListenerRequestEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(RequestEvent::class, RequestListenerRequestEvent::class); } if (!class_exists(RequestListenerControllerEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(ControllerEvent::class, RequestListenerControllerEvent::class); } if (!class_exists(RequestListenerResponseEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(ResponseEvent::class, RequestListenerResponseEvent::class); } if (!class_exists(RequestListenerTerminateEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(TerminateEvent::class, RequestListenerTerminateEvent::class); } if (!class_exists(SubRequestListenerRequestEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(RequestEvent::class, SubRequestListenerRequestEvent::class); } } else { if (!class_exists(ErrorListenerExceptionEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(GetResponseForExceptionEvent::class, ErrorListenerExceptionEvent::class); } if (!class_exists(RequestListenerRequestEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(GetResponseEvent::class, RequestListenerRequestEvent::class); } if (!class_exists(RequestListenerControllerEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(FilterControllerEvent::class, RequestListenerControllerEvent::class); } if (!class_exists(RequestListenerResponseEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(FilterResponseEvent::class, RequestListenerResponseEvent::class); } if (!class_exists(RequestListenerTerminateEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(PostResponseEvent::class, RequestListenerTerminateEvent::class); } if (!class_exists(SubRequestListenerRequestEvent::class, false)) { - /** @psalm-suppress UndefinedClass */ class_alias(GetResponseEvent::class, SubRequestListenerRequestEvent::class); } } if (!interface_exists(DoctrineMiddlewareInterface::class)) { - /** @psalm-suppress UndefinedClass */ class_alias(MiddlewareInterface::class, DoctrineMiddlewareInterface::class); } if (!interface_exists(LegacyExceptionConverterDriverInterface::class)) { - /** @psalm-suppress UndefinedClass */ class_alias(ExceptionConverterDriverInterface::class, LegacyExceptionConverterDriverInterface::class); } + +if (class_exists(Result::class)) { + class_alias(TracingStatementForV3::class, 'Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatement'); +} elseif (interface_exists(Result::class)) { + class_alias(TracingStatementForV2::class, 'Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatement'); +} diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php index 5d934852..4db332ec 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\MockObject\MockObject; use Sentry\SentryBundle\Tests\DoctrineTestCase; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnection; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatement; use Sentry\State\HubInterface; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; @@ -54,9 +55,10 @@ protected function setUp(): void */ public function testPrepare(array $params, array $expectedTags): void { + $sql = 'SELECT 1 + 1'; $statement = $this->createMock(DriverStatementInterface::class); + $resultStatement = new TracingStatement($this->hub, $statement, $sql, $expectedTags); $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); - $sql = 'SELECT 1 + 1'; $transaction = new Transaction(new TransactionContext(), $this->hub); $transaction->initSpanRecorder(); @@ -70,7 +72,7 @@ public function testPrepare(array $params, array $expectedTags): void ->with($sql) ->willReturn($statement); - $this->assertSame($statement, $connection->prepare($sql)); + $this->assertEquals($resultStatement, $connection->prepare($sql)); $spans = $transaction->getSpanRecorder()->getSpans(); @@ -83,8 +85,9 @@ public function testPrepare(array $params, array $expectedTags): void public function testPrepareDoesNothingIfNoSpanIsSetOnHub(): void { - $statement = $this->createMock(DriverStatementInterface::class); $sql = 'SELECT 1 + 1'; + $statement = $this->createMock(DriverStatementInterface::class); + $resultStatement = new TracingStatement($this->hub, $statement, $sql, ['db.system' => 'foo_platform']); $this->hub->expects($this->once()) ->method('getSpan') @@ -95,7 +98,7 @@ public function testPrepareDoesNothingIfNoSpanIsSetOnHub(): void ->with($sql) ->willReturn($statement); - $this->assertSame($statement, $this->connection->prepare($sql)); + $this->assertEquals($resultStatement, $this->connection->prepare($sql)); } /** diff --git a/tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php b/tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php new file mode 100644 index 00000000..e923a490 --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php @@ -0,0 +1,197 @@ +hub = $this->createMock(HubInterface::class); + $this->decoratedStatement = $this->createMock(Statement::class); + $this->statement = new TracingStatementForV2($this->hub, $this->decoratedStatement, 'SELECT 1', ['db.system' => 'sqlite']); + } + + public function testGetIterator(): void + { + $this->assertSame($this->decoratedStatement, $this->statement->getIterator()); + } + + public function testCloseCursor(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('closeCursor') + ->willReturn(true); + + $this->assertTrue($this->statement->closeCursor()); + } + + public function testColumnCount(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('columnCount') + ->willReturn(10); + + $this->assertSame(10, $this->statement->columnCount()); + } + + public function testSetFetchMode(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('setFetchMode') + ->with(FetchMode::COLUMN, 'foo', 'bar') + ->willReturn(true); + + $this->assertTrue($this->statement->setFetchMode(FetchMode::COLUMN, 'foo', 'bar')); + } + + public function testFetch(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('fetch') + ->with(FetchMode::COLUMN, \PDO::FETCH_ORI_NEXT, 10) + ->willReturn('foo'); + + $this->assertSame('foo', $this->statement->fetch(FetchMode::COLUMN, \PDO::FETCH_ORI_NEXT, 10)); + } + + public function testFetchAll(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('fetchAll') + ->with(FetchMode::COLUMN, 0, []) + ->willReturn(['foo']); + + $this->assertSame(['foo'], $this->statement->fetchAll(FetchMode::COLUMN, 0, [])); + } + + public function testFetchColumn(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('fetchColumn') + ->willReturn('foo'); + + $this->assertSame('foo', $this->statement->fetchColumn()); + } + + public function testBindValue(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('bindValue') + ->with('foo', 'bar', ParameterType::INTEGER) + ->willReturn(true); + + $this->assertTrue($this->statement->bindValue('foo', 'bar', ParameterType::INTEGER)); + } + + public function testBindParam(): void + { + $variable = 'bar'; + + $this->decoratedStatement->expects($this->once()) + ->method('bindParam') + ->with('foo', $variable, ParameterType::INTEGER) + ->willReturn(true); + + $this->assertTrue($this->statement->bindParam('foo', $variable, ParameterType::INTEGER)); + } + + public function testErrorCode(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('errorCode') + ->willReturn(false); + + $this->assertFalse($this->statement->errorCode()); + } + + public function testErrorInfo(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('errorInfo') + ->willReturn(['foo' => 'bar']); + + $this->assertSame(['foo' => 'bar'], $this->statement->errorInfo()); + } + + public function testExecute(): void + { + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedStatement->expects($this->once()) + ->method('execute') + ->with(['foo' => 'bar']) + ->willReturn(true); + + $this->assertTrue($this->statement->execute(['foo' => 'bar'])); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingStatementForV2::SPAN_OP_STMT_EXECUTE, $spans[1]->getOp()); + $this->assertSame('SELECT 1', $spans[1]->getDescription()); + $this->assertSame(['db.system' => 'sqlite'], $spans[1]->getTags()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testExecuteDoesNothingIfNoSpanIsSetOnHub(): void + { + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedStatement->expects($this->once()) + ->method('execute') + ->with(['foo' => 'bar']) + ->willReturn(true); + + $this->assertTrue($this->statement->execute(['foo' => 'bar'])); + } + + public function testRowCount(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('rowCount') + ->willReturn(10); + + $this->assertSame(10, $this->statement->rowCount()); + } +} diff --git a/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php b/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php new file mode 100644 index 00000000..f6102acb --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php @@ -0,0 +1,111 @@ += 3.0.'); + } + } + + protected function setUp(): void + { + $this->hub = $this->createMock(HubInterface::class); + $this->decoratedStatement = $this->createMock(Statement::class); + $this->statement = new TracingStatementForV3($this->hub, $this->decoratedStatement, 'SELECT 1', ['db.system' => 'sqlite']); + } + + public function testBindValue(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('bindValue') + ->with('foo', 'bar', ParameterType::INTEGER) + ->willReturn(true); + + $this->assertTrue($this->statement->bindValue('foo', 'bar', ParameterType::INTEGER)); + } + + public function testBindParam(): void + { + $variable = 'bar'; + + $this->decoratedStatement->expects($this->once()) + ->method('bindParam') + ->with('foo', $variable, ParameterType::INTEGER) + ->willReturn(true); + + $this->assertTrue($this->statement->bindParam('foo', $variable, ParameterType::INTEGER)); + } + + public function testExecute(): void + { + $driverResult = $this->createMock(Result::class); + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedStatement->expects($this->once()) + ->method('execute') + ->with(['foo' => 'bar']) + ->willReturn($driverResult); + + $this->assertSame($driverResult, $this->statement->execute(['foo' => 'bar'])); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingStatementForV3::SPAN_OP_STMT_EXECUTE, $spans[1]->getOp()); + $this->assertSame('SELECT 1', $spans[1]->getDescription()); + $this->assertSame(['db.system' => 'sqlite'], $spans[1]->getTags()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testExecuteDoesNothingIfNoSpanIsSetOnHub(): void + { + $driverResult = $this->createMock(Result::class); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedStatement->expects($this->once()) + ->method('execute') + ->with(['foo' => 'bar']) + ->willReturn($driverResult); + + $this->assertSame($driverResult, $this->statement->execute(['foo' => 'bar'])); + } +}