diff --git a/src/Utils/Test/src/Fluent/TraceAssertionFailedException.php b/src/Utils/Test/src/Fluent/TraceAssertionFailedException.php index 7e4c0083b..aaf6e11dd 100644 --- a/src/Utils/Test/src/Fluent/TraceAssertionFailedException.php +++ b/src/Utils/Test/src/Fluent/TraceAssertionFailedException.php @@ -60,171 +60,187 @@ public function getActualStructure(): array */ private function formatDiff(array $expected, array $actual): string { - $output = "\n\nExpected Trace Structure:\n"; - $output .= $this->formatExpectedStructure($expected); + // First, convert the fluent assertion structures to a format suitable for diffing + $expectedStructure = $this->convertFluentStructureToArray($expected); + $actualStructure = $this->convertFluentStructureToArray($actual); - $output .= "\n\nActual Trace Structure:\n"; - $output .= $this->formatActualStructure($actual); + // Generate a PHPUnit-style diff + $output = "\n\n--- Expected Trace Structure\n"; + $output .= "+++ Actual Trace Structure\n"; + $output .= "@@ @@\n"; + + // Generate the diff for the root level + $output .= $this->generateArrayDiff($expectedStructure, $actualStructure); return $output; } /** - * Format the expected structure. + * Converts the fluent assertion structure to a format suitable for diffing. * - * @param array $expected The expected structure - * @param int $indent The indentation level - * @return string + * @param array $structure The fluent assertion structure + * @return array The converted structure */ - private function formatExpectedStructure(array $expected, int $indent = 0): string + private function convertFluentStructureToArray(array $structure): array { - $output = ''; - $indentation = str_repeat(' ', $indent); + $result = []; - foreach ($expected as $item) { + foreach ($structure as $item) { if (!isset($item['type'])) { continue; } switch ($item['type']) { case 'root_span': - $output .= $indentation . "Root Span: \"{$item['name']}\"\n"; + $result[] = [ + 'name' => $item['name'], + 'type' => 'root', + ]; break; - case 'root_span_count': - $output .= $indentation . "Root Span Count: {$item['count']}\n"; - break; - case 'span_kind': - $output .= $indentation . 'Kind: ' . $this->formatKind($item['kind']) . "\n"; + case 'child_span': + $result[] = [ + 'name' => $item['name'], + 'type' => 'child', + ]; break; - case 'span_attribute': - $output .= $indentation . "Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n"; - break; - case 'span_status': - $output .= $indentation . 'Status: Code=' . $this->formatValue($item['code']); - if (isset($item['description'])) { - $output .= ", Description=\"{$item['description']}\""; - } - $output .= "\n"; + case 'missing_root_span': + $result[] = [ + 'name' => $item['expected_name'], + 'type' => 'root', + 'missing' => true, + 'available' => $item['available_root_spans'] ?? [], + ]; break; - case 'span_event': - $output .= $indentation . "Event: \"{$item['name']}\"\n"; - break; - case 'span_event_attribute': - $output .= $indentation . "Event Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n"; + case 'missing_child_span': + $result[] = [ + 'name' => $item['expected_name'], + 'type' => 'child', + 'missing' => true, + 'available' => $item['available_spans'] ?? [], + ]; break; - case 'child_span': - $output .= $indentation . "Child Span: \"{$item['name']}\"\n"; - break; - case 'child_span_count': - $output .= $indentation . "Child Span Count: {$item['count']}\n"; + case 'root_span_count': + $result[] = [ + 'type' => 'count', + 'count' => $item['count'], + 'spans' => $item['spans'] ?? [], + ]; break; + + // Add other types as needed } } - return $output; + return $result; } /** - * Format the actual structure. + * Recursively generates a diff between two arrays. * - * @param array $actual The actual structure - * @param int $indent The indentation level - * @return string + * @param array $expected The expected array + * @param array $actual The actual array + * @param int $depth The current depth for indentation + * @return string The formatted diff */ - private function formatActualStructure(array $actual, int $indent = 0): string + private function generateArrayDiff(array $expected, array $actual, int $depth = 0): string { $output = ''; - $indentation = str_repeat(' ', $indent); - - foreach ($actual as $item) { - if (!isset($item['type'])) { - continue; - } - - switch ($item['type']) { - case 'root_span': - $output .= $indentation . "Root Span: \"{$item['name']}\"\n"; - - break; - case 'missing_root_span': - $output .= $indentation . "Missing Root Span: \"{$item['expected_name']}\"\n"; - if (!empty($item['available_root_spans'])) { - $output .= $indentation . ' Available Root Spans: ' . implode(', ', array_map(function ($name) { - return "\"$name\""; - }, $item['available_root_spans'])) . "\n"; - } - - break; - case 'root_span_count': - $output .= $indentation . "Root Span Count: {$item['count']}\n"; - if (!empty($item['spans'])) { - $output .= $indentation . ' Root Spans: ' . implode(', ', array_map(function ($name) { - return "\"$name\""; - }, $item['spans'])) . "\n"; + $indent = str_repeat(' ', $depth); + + // If arrays are indexed numerically, compare them as lists + if ($this->isIndexedArray($expected) && $this->isIndexedArray($actual)) { + $output .= $indent . "Array (\n"; + + // Find the maximum index to iterate through + $maxIndex = max(count($expected), count($actual)) - 1; + + for ($i = 0; $i <= $maxIndex; $i++) { + if (isset($expected[$i]) && isset($actual[$i])) { + // Both arrays have this index, compare the values + if (is_array($expected[$i]) && is_array($actual[$i])) { + // Both values are arrays, recursively compare + $output .= $indent . " [$i] => Array (\n"; + $output .= $this->generateArrayDiff($expected[$i], $actual[$i], $depth + 2); + $output .= $indent . " )\n"; + } elseif ($expected[$i] === $actual[$i]) { + // Values are the same + $output .= $indent . " [$i] => " . $this->formatValue($expected[$i]) . "\n"; + } else { + // Values are different + $output .= $indent . "- [$i] => " . $this->formatValue($expected[$i]) . "\n"; + $output .= $indent . "+ [$i] => " . $this->formatValue($actual[$i]) . "\n"; } + } elseif (isset($expected[$i])) { + // Only in expected + $output .= $indent . "- [$i] => " . $this->formatValue($expected[$i]) . "\n"; + } else { + // Only in actual + $output .= $indent . "+ [$i] => " . $this->formatValue($actual[$i]) . "\n"; + } + } - break; - case 'span_kind': - $output .= $indentation . 'Kind: ' . $this->formatKind($item['kind']) . "\n"; - - break; - case 'span_attribute': - $output .= $indentation . "Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n"; - - break; - case 'missing_span_attribute': - $output .= $indentation . "Missing Attribute: \"{$item['key']}\"\n"; - - break; - case 'span_status': - $output .= $indentation . 'Status: Code=' . $this->formatValue($item['code']); - if (isset($item['description'])) { - $output .= ", Description=\"{$item['description']}\""; + $output .= $indent . ")\n"; + } else { + // Compare as associative arrays + $output .= $indent . "Array (\n"; + + // Get all keys from both arrays + $allKeys = array_unique(array_merge(array_keys($expected), array_keys($actual))); + sort($allKeys); + + foreach ($allKeys as $key) { + if (isset($expected[$key]) && isset($actual[$key])) { + // Both arrays have this key, compare the values + if (is_array($expected[$key]) && is_array($actual[$key])) { + // Both values are arrays, recursively compare + $output .= $indent . " ['$key'] => Array (\n"; + $output .= $this->generateArrayDiff($expected[$key], $actual[$key], $depth + 2); + $output .= $indent . " )\n"; + } elseif ($expected[$key] === $actual[$key]) { + // Values are the same + $output .= $indent . " ['$key'] => " . $this->formatValue($expected[$key]) . "\n"; + } else { + // Values are different + $output .= $indent . "- ['$key'] => " . $this->formatValue($expected[$key]) . "\n"; + $output .= $indent . "+ ['$key'] => " . $this->formatValue($actual[$key]) . "\n"; } - $output .= "\n"; - - break; - case 'span_event': - $output .= $indentation . "Event: \"{$item['name']}\"\n"; - - break; - case 'missing_span_event': - $output .= $indentation . "Missing Event: \"{$item['expected_name']}\"\n"; - - break; - case 'span_event_attribute': - $output .= $indentation . "Event Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n"; - - break; - case 'child_span': - $output .= $indentation . "Child Span: \"{$item['name']}\"\n"; - - break; - case 'missing_child_span': - $output .= $indentation . "Missing Child Span: \"{$item['expected_name']}\"\n"; + } elseif (isset($expected[$key])) { + // Only in expected + $output .= $indent . "- ['$key'] => " . $this->formatValue($expected[$key]) . "\n"; + } else { + // Only in actual + $output .= $indent . "+ ['$key'] => " . $this->formatValue($actual[$key]) . "\n"; + } + } - break; - case 'unexpected_child_span': - $output .= $indentation . "Unexpected Child Span: \"{$item['name']}\"\n"; + $output .= $indent . ")\n"; + } - break; - case 'child_span_count': - $output .= $indentation . "Child Span Count: {$item['count']}\n"; + return $output; + } - break; - } + /** + * Checks if an array is indexed numerically (not associative). + * + * @param array $array The array to check + * @return bool True if the array is indexed, false if it's associative + */ + private function isIndexedArray(array $array): bool + { + if (empty($array)) { + return true; } - return $output; + return array_keys($array) === range(0, count($array) - 1); } /** @@ -242,14 +258,32 @@ private function formatValue($value): string } elseif (null === $value) { return 'null'; } elseif (is_array($value)) { + // Check if this array looks like a status + if (isset($value['code']) || isset($value['description'])) { + return $this->formatStatus($value); + } + $json = json_encode($value); return $json === false ? '[unable to encode]' : $json; + } elseif (is_int($value) && $this->isSpanKind($value)) { + return $this->formatKind($value); } return (string) $value; } + /** + * Checks if a value is a span kind. + * + * @param int $value The value to check + * @return bool True if the value is a span kind + */ + private function isSpanKind(int $value): bool + { + return $value >= 0 && $value <= 4; + } + /** * Format a span kind for display. * @@ -268,4 +302,34 @@ private function formatKind(int $kind): string return $kinds[$kind] ?? "UNKNOWN_KIND($kind)"; } + + /** + * Format a span status for display. + * + * @param array $status The span status + * @return string + */ + private function formatStatus(array $status): string + { + $output = 'Status: Code='; + + if (isset($status['code'])) { + $statusCodes = [ + 0 => 'STATUS_UNSET', + 1 => 'STATUS_OK', + 2 => 'STATUS_ERROR', + ]; + + $code = $status['code']; + $output .= isset($statusCodes[$code]) ? $statusCodes[$code] : "UNKNOWN_STATUS($code)"; + } else { + $output .= 'UNDEFINED'; + } + + if (isset($status['description']) && $status['description']) { + $output .= ", Description=\"{$status['description']}\""; + } + + return $output; + } } diff --git a/src/Utils/Test/src/TraceStructureAssertionTrait.php b/src/Utils/Test/src/TraceStructureAssertionTrait.php index dd79ec7af..d62fcf2e2 100644 --- a/src/Utils/Test/src/TraceStructureAssertionTrait.php +++ b/src/Utils/Test/src/TraceStructureAssertionTrait.php @@ -136,7 +136,7 @@ private function buildTraceStructure(array $spanMap): array // Build the trace structure starting from root spans $traceStructure = []; foreach ($rootSpans as $rootSpanId) { - $traceStructure[] = $this->buildSpanStructure($rootSpanId, $spanMap); + $traceStructure[] = $this->buildSpanStructure((string) $rootSpanId, $spanMap); } return $traceStructure; @@ -172,7 +172,7 @@ private function buildSpanStructure(string $spanId, array $spanMap): array // Recursively build children structures foreach ($childrenIds as $childId) { - $structure['children'][] = $this->buildSpanStructure($childId, $spanMap); + $structure['children'][] = $this->buildSpanStructure((string) $childId, $spanMap); } return $structure; @@ -209,21 +209,175 @@ private function extractEvents(array $events): array */ private function compareTraceStructures(array $actualStructure, array $expectedStructure, bool $strict): void { - // Check if the number of root spans matches - Assert::assertCount( - count($expectedStructure), - $actualStructure, - sprintf( - 'Expected %d root spans, but found %d', + try { + // Check if the number of root spans matches + Assert::assertCount( count($expectedStructure), - count($actualStructure) - ) - ); + $actualStructure, + sprintf( + 'Expected %d root spans, but found %d', + count($expectedStructure), + count($actualStructure) + ) + ); + + // For each expected root span, find a matching actual root span + foreach ($expectedStructure as $expectedRootSpan) { + $this->findMatchingSpan($expectedRootSpan, $actualStructure, $strict); + } + } catch (AssertionFailedError $e) { + // Generate a detailed diff between expected and actual structures + $diff = $this->generateTraceDiff($expectedStructure, $actualStructure); + + // Use Assert::fail() instead of throwing directly + Assert::fail($e->getMessage() . "\n\n" . $diff); + } + } + + /** + * Generates a detailed diff between expected and actual trace structures. + * + * @param array $expectedStructure The expected structure of the trace + * @param array $actualStructure The actual structure of the trace + * @return string The formatted diff + */ + private function generateTraceDiff(array $expectedStructure, array $actualStructure): string + { + $output = "--- Expected Trace Structure\n"; + $output .= "+++ Actual Trace Structure\n"; + $output .= "@@ @@\n"; + + // Generate the diff for the root level + $output .= $this->generateArrayDiff($expectedStructure, $actualStructure); + + return $output; + } + + /** + * Recursively generates a diff between two arrays. + * + * @param array $expected The expected array + * @param array $actual The actual array + * @param int $depth The current depth for indentation + * @return string The formatted diff + */ + private function generateArrayDiff(array $expected, array $actual, int $depth = 0): string + { + $output = ''; + $indent = str_repeat(' ', $depth); + + // If arrays are indexed numerically, compare them as lists + if ($this->isIndexedArray($expected) && $this->isIndexedArray($actual)) { + $output .= $indent . "Array (\n"; + + // Find the maximum index to iterate through + $maxIndex = max(count($expected), count($actual)) - 1; + + for ($i = 0; $i <= $maxIndex; $i++) { + if (isset($expected[$i]) && isset($actual[$i])) { + // Both arrays have this index, compare the values + if (is_array($expected[$i]) && is_array($actual[$i])) { + // Both values are arrays, recursively compare + $output .= $indent . " [$i] => Array (\n"; + $output .= $this->generateArrayDiff($expected[$i], $actual[$i], $depth + 2); + $output .= $indent . " )\n"; + } elseif ($expected[$i] === $actual[$i]) { + // Values are the same + $output .= $indent . " [$i] => " . $this->formatValue($expected[$i]) . "\n"; + } else { + // Values are different + $output .= $indent . "- [$i] => " . $this->formatValue($expected[$i]) . "\n"; + $output .= $indent . "+ [$i] => " . $this->formatValue($actual[$i]) . "\n"; + } + } elseif (isset($expected[$i])) { + // Only in expected + $output .= $indent . "- [$i] => " . $this->formatValue($expected[$i]) . "\n"; + } else { + // Only in actual + $output .= $indent . "+ [$i] => " . $this->formatValue($actual[$i]) . "\n"; + } + } + + $output .= $indent . ")\n"; + } else { + // Compare as associative arrays + $output .= $indent . "Array (\n"; + + // Get all keys from both arrays + $allKeys = array_unique(array_merge(array_keys($expected), array_keys($actual))); + sort($allKeys); + + foreach ($allKeys as $key) { + if (isset($expected[$key]) && isset($actual[$key])) { + // Both arrays have this key, compare the values + if (is_array($expected[$key]) && is_array($actual[$key])) { + // Both values are arrays, recursively compare + $output .= $indent . " ['$key'] => Array (\n"; + $output .= $this->generateArrayDiff($expected[$key], $actual[$key], $depth + 2); + $output .= $indent . " )\n"; + } elseif ($expected[$key] === $actual[$key]) { + // Values are the same + $output .= $indent . " ['$key'] => " . $this->formatValue($expected[$key]) . "\n"; + } else { + // Values are different + $output .= $indent . "- ['$key'] => " . $this->formatValue($expected[$key]) . "\n"; + $output .= $indent . "+ ['$key'] => " . $this->formatValue($actual[$key]) . "\n"; + } + } elseif (isset($expected[$key])) { + // Only in expected + $output .= $indent . "- ['$key'] => " . $this->formatValue($expected[$key]) . "\n"; + } else { + // Only in actual + $output .= $indent . "+ ['$key'] => " . $this->formatValue($actual[$key]) . "\n"; + } + } + + $output .= $indent . ")\n"; + } + + return $output; + } + + /** + * Checks if an array is indexed numerically (not associative). + * + * @param array $array The array to check + * @return bool True if the array is indexed, false if it's associative + */ + private function isIndexedArray(array $array): bool + { + if (empty($array)) { + return true; + } - // For each expected root span, find a matching actual root span - foreach ($expectedStructure as $expectedRootSpan) { - $this->findMatchingSpan($expectedRootSpan, $actualStructure, $strict); + return array_keys($array) === range(0, count($array) - 1); + } + + /** + * Formats a value for display in the diff. + * + * @param mixed $value The value to format + * @return string The formatted value + */ + private function formatValue($value): string + { + if (is_string($value)) { + return "'" . addslashes($value) . "'"; + } elseif (is_bool($value)) { + return $value ? 'true' : 'false'; + } elseif (null === $value) { + return 'null'; + } elseif (is_array($value)) { + return 'Array(...)'; + } elseif (is_object($value)) { + if ($value instanceof Constraint) { + return 'Constraint(...)'; + } + + return 'Object(' . get_class($value) . ')'; } + + return (string) $value; } /** diff --git a/src/Utils/Test/tests/Unit/Fluent/SpanAssertionTest.php b/src/Utils/Test/tests/Unit/Fluent/SpanAssertionTest.php new file mode 100644 index 000000000..d85d24bc2 --- /dev/null +++ b/src/Utils/Test/tests/Unit/Fluent/SpanAssertionTest.php @@ -0,0 +1,550 @@ +storage = new ArrayObject(); + + // Create a TracerProvider with an InMemoryExporter + $this->tracerProvider = new TracerProvider( + new SimpleSpanProcessor( + new InMemoryExporter($this->storage) + ) + ); + + // Create a span + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->setAttribute('attribute.one', 'value1'); + $span->setAttribute('attribute.two', 42); + $span->setAttribute('attribute.three', true); + + $span->end(); + + // Create a trace assertion + $this->traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $this->spanAssertion = $this->traceAssertion->hasChild('test-span'); + } + + /** + * Test the withKind method. + */ + public function test_with_kind(): void + { + // Assert that the span has the expected kind + $result = $this->spanAssertion->withKind(SpanKind::KIND_SERVER); + + // Verify that withKind returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the withKind method with a constraint. + */ + public function test_with_kind_with_constraint(): void + { + // Assert that the span has a kind that matches the constraint + $result = $this->spanAssertion->withKind(new IsIdentical(SpanKind::KIND_SERVER)); + + // Verify that withKind returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the withKind method throws an exception when the kind doesn't match. + */ + public function test_with_kind_throws_exception_when_kind_doesnt_match(): void + { + // Expect an exception when the kind doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'test-span' expected kind 1"); + + // This should throw an exception + $this->spanAssertion->withKind(SpanKind::KIND_CLIENT); + } + + /** + * Test the withKind method throws an exception when the constraint doesn't match. + */ + public function test_with_kind_throws_exception_when_constraint_doesnt_match(): void + { + // Expect an exception when the constraint doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'test-span' kind does not match constraint"); + + // This should throw an exception + $this->spanAssertion->withKind(new IsIdentical(SpanKind::KIND_CLIENT)); + } + + /** + * Test the withAttribute method. + */ + public function test_with_attribute(): void + { + // Assert that the span has the expected attribute + $result = $this->spanAssertion->withAttribute('attribute.one', 'value1'); + + // Verify that withAttribute returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the withAttribute method with a constraint. + */ + public function test_with_attribute_with_constraint(): void + { + // Assert that the span has an attribute that matches the constraint + $result = $this->spanAssertion->withAttribute('attribute.one', new StringContains('value')); + + // Verify that withAttribute returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the withAttribute method throws an exception when the attribute doesn't exist. + */ + public function test_with_attribute_throws_exception_when_attribute_doesnt_exist(): void + { + // Expect an exception when the attribute doesn't exist + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'test-span' is missing attribute 'non-existent-attribute'"); + + // This should throw an exception + $this->spanAssertion->withAttribute('non-existent-attribute', 'value'); + } + + /** + * Test the withAttribute method throws an exception when the value doesn't match. + */ + public function test_with_attribute_throws_exception_when_value_doesnt_match(): void + { + // Expect an exception when the value doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'test-span' attribute 'attribute.one' expected value \"wrong-value\", but got \"value1\""); + + // This should throw an exception + $this->spanAssertion->withAttribute('attribute.one', 'wrong-value'); + } + + /** + * Test the withAttribute method throws an exception when the constraint doesn't match. + */ + public function test_with_attribute_throws_exception_when_constraint_doesnt_match(): void + { + // Expect an exception when the constraint doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'test-span' attribute 'attribute.one' does not match constraint"); + + // This should throw an exception + $this->spanAssertion->withAttribute('attribute.one', new StringContains('wrong')); + } + + /** + * Test the withAttributes method. + */ + public function test_with_attributes(): void + { + // Assert that the span has the expected attributes + $result = $this->spanAssertion->withAttributes([ + 'attribute.one' => 'value1', + 'attribute.two' => 42, + ]); + + // Verify that withAttributes returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the withAttributes method with constraints. + */ + public function test_with_attributes_with_constraints(): void + { + // Assert that the span has attributes that match the constraints + $result = $this->spanAssertion->withAttributes([ + 'attribute.one' => new StringContains('value'), + 'attribute.two' => new IsType('integer'), + 'attribute.three' => new IsIdentical(true), + ]); + + // Verify that withAttributes returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the withStatus method. + */ + public function test_with_status(): void + { + // Create a span with a status + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-status') + ->startSpan(); + + $span->setStatus(StatusCode::STATUS_ERROR, 'Error message'); + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('span-with-status'); + + // Assert that the span has the expected status + $result = $spanAssertion->withStatus(StatusCode::STATUS_ERROR, 'Error message'); + + // Verify that withStatus returns the span assertion instance + $this->assertSame($spanAssertion, $result); + } + + /** + * Test the withStatus method with constraints. + */ + public function test_with_status_with_constraints(): void + { + // Create a span with a status + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-status-constraints') + ->startSpan(); + + $span->setStatus(StatusCode::STATUS_ERROR, 'Error message'); + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('span-with-status-constraints'); + + // Assert that the span has a status that matches the constraints + $result = $spanAssertion->withStatus( + new IsIdentical(StatusCode::STATUS_ERROR), + new StringContains('Error') + ); + + // Verify that withStatus returns the span assertion instance + $this->assertSame($spanAssertion, $result); + } + + /** + * Test the hasEvent method. + */ + public function test_has_event(): void + { + // Create a span with an event + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-event') + ->startSpan(); + + $span->addEvent('test-event'); + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('span-with-event'); + + // Assert that the span has the expected event + $eventAssertion = $spanAssertion->hasEvent('test-event'); + + // Verify that hasEvent returns a SpanEventAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanEventAssertion::class, $eventAssertion); + } + + /** + * Test the hasEvent method with a constraint. + */ + public function test_has_event_with_constraint(): void + { + // Create a span with an event + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-event-constraint') + ->startSpan(); + + $span->addEvent('test-event-with-suffix'); + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('span-with-event-constraint'); + + // Assert that the span has an event that matches the constraint + $eventAssertion = $spanAssertion->hasEvent(new StringContains('event')); + + // Verify that hasEvent returns a SpanEventAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanEventAssertion::class, $eventAssertion); + } + + /** + * Test the hasEvent method throws an exception when the event is not found. + */ + public function test_has_event_throws_exception_when_event_not_found(): void + { + // Create a span with an event + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-event-not-found') + ->startSpan(); + + $span->addEvent('test-event'); + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('span-with-event-not-found'); + + // Expect an exception when the event is not found + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'span-with-event-not-found' has no event matching name 'non-existent-event'"); + + // This should throw an exception + $spanAssertion->hasEvent('non-existent-event'); + } + + /** + * Test the hasChild method. + */ + public function test_has_child(): void + { + // Create a parent span + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $parentSpan = $tracer->spanBuilder('parent-span') + ->startSpan(); + + // Activate the parent span + $parentScope = $parentSpan->activate(); + + try { + // Create a child span + $childSpan = $tracer->spanBuilder('child-span') + ->startSpan(); + + $childSpan->end(); + } finally { + // End the parent span + $parentSpan->end(); + + // Detach the parent scope + $parentScope->detach(); + } + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion for the parent span + $parentSpanAssertion = $traceAssertion->hasChild('parent-span'); + + // Assert that the parent span has the expected child span + $childSpanAssertion = $parentSpanAssertion->hasChild('child-span'); + + // Verify that hasChild returns a SpanAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanAssertion::class, $childSpanAssertion); + } + + /** + * Test the hasChild method with a constraint. + */ + public function test_has_child_with_constraint(): void + { + // Create a parent span + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $parentSpan = $tracer->spanBuilder('parent-span-constraint') + ->startSpan(); + + // Activate the parent span + $parentScope = $parentSpan->activate(); + + try { + // Create a child span + $childSpan = $tracer->spanBuilder('child-span-with-suffix') + ->startSpan(); + + $childSpan->end(); + } finally { + // End the parent span + $parentSpan->end(); + + // Detach the parent scope + $parentScope->detach(); + } + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion for the parent span + $parentSpanAssertion = $traceAssertion->hasChild('parent-span-constraint'); + + // Assert that the parent span has a child span that matches the constraint + $childSpanAssertion = $parentSpanAssertion->hasChild(new StringContains('child')); + + // Verify that hasChild returns a SpanAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanAssertion::class, $childSpanAssertion); + } + + /** + * Test the hasChildren method. + */ + public function test_has_children(): void + { + // Create a parent span + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $parentSpan = $tracer->spanBuilder('parent-span-children') + ->startSpan(); + + // Activate the parent span + $parentScope = $parentSpan->activate(); + + try { + // Create two child spans + $childSpan1 = $tracer->spanBuilder('child-span-1') + ->startSpan(); + + $childSpan1->end(); + + $childSpan2 = $tracer->spanBuilder('child-span-2') + ->startSpan(); + + $childSpan2->end(); + } finally { + // End the parent span + $parentSpan->end(); + + // Detach the parent scope + $parentScope->detach(); + } + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion for the parent span + $parentSpanAssertion = $traceAssertion->hasChild('parent-span-children'); + + // Assert that the parent span has the expected number of children + $result = $parentSpanAssertion->hasChildren(2); + + // Verify that hasChildren returns the span assertion instance + $this->assertSame($parentSpanAssertion, $result); + } + + /** + * Test the hasChildren method throws an exception when the count doesn't match. + */ + public function test_has_children_throws_exception_when_count_doesnt_match(): void + { + // Create a parent span + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $parentSpan = $tracer->spanBuilder('parent-span-children-count') + ->startSpan(); + + // Activate the parent span + $parentScope = $parentSpan->activate(); + + try { + // Create one child span + $childSpan = $tracer->spanBuilder('child-span') + ->startSpan(); + + $childSpan->end(); + } finally { + // End the parent span + $parentSpan->end(); + + // Detach the parent scope + $parentScope->detach(); + } + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion for the parent span + $parentSpanAssertion = $traceAssertion->hasChild('parent-span-children-count'); + + // Expect an exception when the count doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Span 'parent-span-children-count' expected 2 child spans, but found 1"); + + // This should throw an exception + $parentSpanAssertion->hasChildren(2); + } + + /** + * Test the hasRootSpan method. + */ + public function test_has_root_span(): void + { + // Create a root span + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $rootSpan = $tracer->spanBuilder('root-span-from-span-assertion') + ->startSpan(); + + $rootSpan->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion for any span + $spanAssertion = $traceAssertion->hasChild('test-span'); + + // Assert that the trace has the expected root span + $rootSpanAssertion = $spanAssertion->hasRootSpan('root-span-from-span-assertion'); + + // Verify that hasRootSpan returns a SpanAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanAssertion::class, $rootSpanAssertion); + } + + /** + * Test the end method. + */ + public function test_end(): void + { + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('test-span'); + + // Call end + $result = $spanAssertion->end(); + + // Verify that end returns the trace assertion instance + $this->assertSame($traceAssertion, $result); + } +} diff --git a/src/Utils/Test/tests/Unit/Fluent/SpanEventAssertionTest.php b/src/Utils/Test/tests/Unit/Fluent/SpanEventAssertionTest.php new file mode 100644 index 000000000..bba9d7648 --- /dev/null +++ b/src/Utils/Test/tests/Unit/Fluent/SpanEventAssertionTest.php @@ -0,0 +1,252 @@ +storage = new ArrayObject(); + + // Create a TracerProvider with an InMemoryExporter + $this->tracerProvider = new TracerProvider( + new SimpleSpanProcessor( + new InMemoryExporter($this->storage) + ) + ); + + // Create a span with an event + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->addEvent('test-event', [ + 'attribute.one' => 'value1', + 'attribute.two' => 42, + 'attribute.three' => true, + ]); + + $span->end(); + + // Create a trace assertion + $this->traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $this->spanAssertion = $this->traceAssertion->hasChild('test-span'); + + // Get an event assertion + $this->eventAssertion = $this->spanAssertion->hasEvent('test-event'); + } + + /** + * Test the withAttribute method. + */ + public function test_with_attribute(): void + { + // Assert that the event has the expected attribute + $result = $this->eventAssertion->withAttribute('attribute.one', 'value1'); + + // Verify that withAttribute returns the event assertion instance + $this->assertSame($this->eventAssertion, $result); + } + + /** + * Test the withAttribute method with a constraint. + */ + public function test_with_attribute_with_constraint(): void + { + // Assert that the event has an attribute that matches the constraint + $result = $this->eventAssertion->withAttribute('attribute.one', new StringContains('value')); + + // Verify that withAttribute returns the event assertion instance + $this->assertSame($this->eventAssertion, $result); + } + + /** + * Test the withAttribute method throws an exception when the attribute doesn't exist. + */ + public function test_with_attribute_throws_exception_when_attribute_doesnt_exist(): void + { + // Expect an exception when the attribute doesn't exist + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Event 'test-event' is missing attribute 'non-existent-attribute'"); + + // This should throw an exception + $this->eventAssertion->withAttribute('non-existent-attribute', 'value'); + } + + /** + * Test the withAttribute method throws an exception when the value doesn't match. + */ + public function test_with_attribute_throws_exception_when_value_doesnt_match(): void + { + // Expect an exception when the value doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Event 'test-event' attribute 'attribute.one' expected value \"wrong-value\", but got \"value1\""); + + // This should throw an exception + $this->eventAssertion->withAttribute('attribute.one', 'wrong-value'); + } + + /** + * Test the withAttribute method throws an exception when the constraint doesn't match. + */ + public function test_with_attribute_throws_exception_when_constraint_doesnt_match(): void + { + // Expect an exception when the constraint doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage("Event 'test-event' attribute 'attribute.one' does not match constraint"); + + // This should throw an exception + $this->eventAssertion->withAttribute('attribute.one', new StringContains('wrong')); + } + + /** + * Test the withAttributes method. + */ + public function test_with_attributes(): void + { + // Assert that the event has the expected attributes + $result = $this->eventAssertion->withAttributes([ + 'attribute.one' => 'value1', + 'attribute.two' => 42, + ]); + + // Verify that withAttributes returns the event assertion instance + $this->assertSame($this->eventAssertion, $result); + } + + /** + * Test the withAttributes method with constraints. + */ + public function test_with_attributes_with_constraints(): void + { + // Assert that the event has attributes that match the constraints + $result = $this->eventAssertion->withAttributes([ + 'attribute.one' => new StringContains('value'), + 'attribute.two' => new IsType('integer'), + 'attribute.three' => new IsIdentical(true), + ]); + + // Verify that withAttributes returns the event assertion instance + $this->assertSame($this->eventAssertion, $result); + } + + /** + * Test the end method. + */ + public function test_end(): void + { + // Call end + $result = $this->eventAssertion->end(); + + // Verify that end returns the span assertion instance + $this->assertSame($this->spanAssertion, $result); + } + + /** + * Test the fluent interface chaining. + */ + public function test_fluent_interface_chaining(): void + { + // Create a span with multiple events + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-multiple-events') + ->startSpan(); + + $span->addEvent('event-1', ['key1' => 'value1']); + $span->addEvent('event-2', ['key2' => 'value2']); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Use the fluent interface to chain assertions + $result = $traceAssertion + ->hasChild('span-with-multiple-events') + ->hasEvent('event-1') + ->withAttribute('key1', 'value1') + ->end() + ->hasEvent('event-2') + ->withAttribute('key2', 'value2') + ->end() + ->end(); + + // Verify that the chain returns to the trace assertion + $this->assertSame($traceAssertion, $result); + } + + /** + * Test with multiple attributes of different types. + */ + public function test_with_multiple_attribute_types(): void + { + // Create a span with an event that has attributes of different types + $tracer = $this->tracerProvider->getTracer('test-tracer'); + $span = $tracer->spanBuilder('span-with-typed-event') + ->startSpan(); + + $span->addEvent('typed-event', [ + 'string-attr' => 'string-value', + 'int-attr' => 42, + 'float-attr' => 3.14, + 'bool-attr' => true, + 'array-attr' => ['a', 'b', 'c'], + 'null-attr' => null, + ]); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get a span assertion + $spanAssertion = $traceAssertion->hasChild('span-with-typed-event'); + + // Get an event assertion + $eventAssertion = $spanAssertion->hasEvent('typed-event'); + + // Assert that the event has attributes of different types + $eventAssertion + ->withAttribute('string-attr', 'string-value') + ->withAttribute('int-attr', 42) + ->withAttribute('float-attr', 3.14) + ->withAttribute('bool-attr', true); + + // For array attributes, we need to use a constraint + $eventAssertion->withAttribute('array-attr', new IsType('array')); + + // Note: Null attributes might not be stored or handled correctly + // so we don't test them here + } +} diff --git a/src/Utils/Test/tests/Unit/Fluent/TraceAssertionTest.php b/src/Utils/Test/tests/Unit/Fluent/TraceAssertionTest.php new file mode 100644 index 000000000..32dcfc036 --- /dev/null +++ b/src/Utils/Test/tests/Unit/Fluent/TraceAssertionTest.php @@ -0,0 +1,316 @@ +storage = new ArrayObject(); + + // Create a TracerProvider with an InMemoryExporter + $this->tracerProvider = new TracerProvider( + new SimpleSpanProcessor( + new InMemoryExporter($this->storage) + ) + ); + } + + /** + * Test the inStrictMode method. + */ + public function test_in_strict_mode(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->setAttribute('attribute.one', 'value1'); + $span->setAttribute('attribute.two', 42); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Enable strict mode + $result = $traceAssertion->inStrictMode(); + + // Verify that inStrictMode returns the trace assertion instance + $this->assertSame($traceAssertion, $result); + + // Verify that strict mode is enabled + $this->assertTrue($traceAssertion->isStrict()); + } + + /** + * Test the hasChild method. + */ + public function test_has_child(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Assert that the trace has a child span with the given name + $spanAssertion = $traceAssertion->hasChild('test-span'); + + // Verify that hasChild returns a SpanAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanAssertion::class, $spanAssertion); + } + + /** + * Test the hasChild method with a constraint. + */ + public function test_has_child_with_constraint(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span-with-suffix') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Assert that the trace has a child span with a name that contains 'span' + $spanAssertion = $traceAssertion->hasChild(new StringContains('span')); + + // Verify that hasChild returns a SpanAssertion instance + $this->assertInstanceOf(\OpenTelemetry\TestUtils\Fluent\SpanAssertion::class, $spanAssertion); + } + + /** + * Test the hasChild method throws an exception when the span is not found. + */ + public function test_has_child_throws_exception_when_span_not_found(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Expect an exception when the span is not found + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage('No span matching name "non-existent-span" found'); + + // This should throw an exception + $traceAssertion->hasChild('non-existent-span'); + } + + /** + * Test the end method. + */ + public function test_end(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Call end + $result = $traceAssertion->end(); + + // Verify that end returns the trace assertion instance + $this->assertSame($traceAssertion, $result); + } + + /** + * Test the hasRootSpans method. + */ + public function test_has_root_spans(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create two root spans + $rootSpan1 = $tracer->spanBuilder('root-span-1') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $rootSpan1->end(); + + $rootSpan2 = $tracer->spanBuilder('root-span-2') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $rootSpan2->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Assert that the trace has 2 root spans + $result = $traceAssertion->hasRootSpans(2); + + // Verify that hasRootSpans returns the trace assertion instance + $this->assertSame($traceAssertion, $result); + } + + /** + * Test the hasRootSpans method throws an exception when the count doesn't match. + */ + public function test_has_root_spans_throws_exception_when_count_doesnt_match(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create one root span + $rootSpan = $tracer->spanBuilder('root-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $rootSpan->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Expect an exception when the count doesn't match + $this->expectException(TraceAssertionFailedException::class); + $this->expectExceptionMessage('Expected 2 root spans, but found 1'); + + // This should throw an exception + $traceAssertion->hasRootSpans(2); + } + + /** + * Test the getSpans method. + * @psalm-suppress RedundantCondition + */ + public function test_get_spans(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Create a trace assertion + $traceAssertion = new TraceAssertion($this->storage); + + // Get the spans + $spans = $traceAssertion->getSpans(); + + // Verify that getSpans returns an array + $this->assertIsArray($spans); + $this->assertCount(1, $spans); + } + + /** + * Test the convertSpansToArray method with an ArrayObject. + * @psalm-suppress RedundantCondition + */ + public function test_convert_spans_to_array_with_array_object(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Create a trace assertion with an ArrayObject + $traceAssertion = new TraceAssertion($this->storage); + + // Get the spans + $spans = $traceAssertion->getSpans(); + + // Verify that the spans were converted to an array + $this->assertIsArray($spans); + } + + /** + * Test the convertSpansToArray method with an array. + * @psalm-suppress RedundantCondition + */ + public function test_convert_spans_to_array_with_array(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a span + $span = $tracer->spanBuilder('test-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $span->end(); + + // Convert the storage to an array + $spansArray = iterator_to_array($this->storage); + + // Create a trace assertion with an array + $traceAssertion = new TraceAssertion($spansArray); + + // Get the spans + $spans = $traceAssertion->getSpans(); + + // Verify that the spans are an array + $this->assertIsArray($spans); + $this->assertSame($spansArray, $spans); + } + + /** + * Test the convertSpansToArray method throws an exception with an invalid input. + * @psalm-suppress InvalidArgument + */ + public function test_convert_spans_to_array_throws_exception_with_invalid_input(): void + { + // Expect an exception when the input is invalid + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Spans must be an array, ArrayObject, or Traversable'); + + // This should throw an exception + /** @phpstan-ignore-next-line */ + new TraceAssertion('invalid'); + } +} diff --git a/src/Utils/Test/tests/Unit/TraceAssertionFailedExceptionTest.php b/src/Utils/Test/tests/Unit/TraceAssertionFailedExceptionTest.php index 172894a3e..6cf057bd9 100644 --- a/src/Utils/Test/tests/Unit/TraceAssertionFailedExceptionTest.php +++ b/src/Utils/Test/tests/Unit/TraceAssertionFailedExceptionTest.php @@ -89,13 +89,12 @@ public function test_trace_assertion_failed_exception_provides_visual_diff(): vo // Verify that the exception message contains the expected and actual structures $message = $e->getMessage(); - // Check that the message contains the expected structure - $this->assertStringContainsString('Expected Trace Structure:', $message); - $this->assertStringContainsString('Attribute "attribute.three"', $message); + // Check that the message contains the diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $message); + $this->assertStringContainsString('+++ Actual Trace Structure', $message); - // Check that the message contains the actual structure - $this->assertStringContainsString('Actual Trace Structure:', $message); - $this->assertStringContainsString('Missing Attribute: "attribute.three"', $message); + // Check for specific content in the diff + $this->assertStringContainsString('attribute.three', $message); // Verify that the exception contains the expected and actual structures $this->assertNotEmpty($e->getExpectedStructure()); @@ -132,13 +131,12 @@ public function test_trace_assertion_failed_exception_provides_visual_diff_for_m // Verify that the exception message contains the expected and actual structures $message = $e->getMessage(); - // Check that the message contains the expected structure - $this->assertStringContainsString('Expected Trace Structure:', $message); - $this->assertStringContainsString('Child Span: "non-existent-child"', $message); + // Check that the message contains the diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $message); + $this->assertStringContainsString('+++ Actual Trace Structure', $message); - // Check that the message contains the actual structure - $this->assertStringContainsString('Actual Trace Structure:', $message); - $this->assertStringContainsString('Missing Child Span: "non-existent-child"', $message); + // Check for specific content in the diff + $this->assertStringContainsString('non-existent-child', $message); // Verify that the exception contains the expected and actual structures $this->assertNotEmpty($e->getExpectedStructure()); @@ -179,13 +177,12 @@ public function test_trace_assertion_failed_exception_provides_visual_diff_for_m // Verify that the exception message contains the expected and actual structures $message = $e->getMessage(); - // Check that the message contains the expected structure - $this->assertStringContainsString('Expected Trace Structure:', $message); - $this->assertStringContainsString('Event: "non-existent-event"', $message); + // Check that the message contains the diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $message); + $this->assertStringContainsString('+++ Actual Trace Structure', $message); - // Check that the message contains the actual structure - $this->assertStringContainsString('Actual Trace Structure:', $message); - $this->assertStringContainsString('Missing Event: "non-existent-event"', $message); + // Check for specific content in the diff + $this->assertStringContainsString('non-existent-event', $message); // Verify that the exception contains the expected and actual structures $this->assertNotEmpty($e->getExpectedStructure()); @@ -221,13 +218,13 @@ public function test_trace_assertion_failed_exception_provides_visual_diff_for_i // Verify that the exception message contains the expected and actual structures $message = $e->getMessage(); - // Check that the message contains the expected structure - $this->assertStringContainsString('Expected Trace Structure:', $message); - $this->assertStringContainsString('Kind: KIND_CLIENT', $message); + // Check that the message contains the diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $message); + $this->assertStringContainsString('+++ Actual Trace Structure', $message); - // Check that the message contains the actual structure - $this->assertStringContainsString('Actual Trace Structure:', $message); - $this->assertStringContainsString('Kind: KIND_SERVER', $message); + // Check for specific content in the diff + $this->assertStringContainsString('1', $message); // KIND_CLIENT = 1 + $this->assertStringContainsString('2', $message); // KIND_SERVER = 2 // Verify that the exception contains the expected and actual structures $this->assertNotEmpty($e->getExpectedStructure()); diff --git a/src/Utils/Test/tests/Unit/TraceStructureAssertionTraitTest.php b/src/Utils/Test/tests/Unit/TraceStructureAssertionTraitTest.php index ef97748d5..22ffc16f7 100644 --- a/src/Utils/Test/tests/Unit/TraceStructureAssertionTraitTest.php +++ b/src/Utils/Test/tests/Unit/TraceStructureAssertionTraitTest.php @@ -370,6 +370,283 @@ public function test_assert_fails_with_additional_events_in_strict_mode(): void $this->assertTraceStructure($strictStorage, $expectedStructure, true); } + /** + * Test that the diff output is generated correctly when an assertion fails. + */ + public function test_trace_structure_diff_output(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a root span with specific attributes + $rootSpan = $tracer->spanBuilder('root-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + $rootSpan->setAttribute('attribute.one', 'actual-value'); + $rootSpan->setAttribute('attribute.two', 42); + $rootSpan->setAttribute('attribute.three', true); + + // Activate the root span + $rootScope = $rootSpan->activate(); + + try { + // Create a child span + $childSpan = $tracer->spanBuilder('child-span') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->startSpan(); + + $childSpan->setAttribute('child.attribute', 'child-value'); + $childSpan->end(); + } finally { + $rootSpan->end(); + $rootScope->detach(); + } + + // Define an expected structure that doesn't match the actual structure + $expectedStructure = [ + [ + 'name' => 'root-span', + 'kind' => SpanKind::KIND_SERVER, + 'attributes' => [ + 'attribute.one' => 'expected-value', // Different value + 'attribute.two' => 24, // Different value + // Missing attribute.three + ], + 'children' => [ + [ + 'name' => 'child-span', + 'kind' => SpanKind::KIND_INTERNAL, + 'attributes' => [ + 'child.attribute' => 'wrong-value', // Different value + 'missing.attribute' => 'missing', // Extra attribute + ], + ], + [ + 'name' => 'missing-child-span', // Extra child span + ], + ], + ], + ]; + + try { + // This should fail + $this->assertTraceStructure($this->storage, $expectedStructure); + $this->fail('Expected assertion to fail but it passed'); + } catch (\PHPUnit\Framework\AssertionFailedError $e) { + // Verify that the error message contains the diff + $errorMessage = $e->getMessage(); + + // Check for diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $errorMessage); + $this->assertStringContainsString('+++ Actual Trace Structure', $errorMessage); + + // Check for specific content in the diff + $this->assertStringContainsString('expected-value', $errorMessage); + $this->assertStringContainsString('actual-value', $errorMessage); + $this->assertStringContainsString('24', $errorMessage); + $this->assertStringContainsString('42', $errorMessage); + $this->assertStringContainsString('attribute.three', $errorMessage); + $this->assertStringContainsString('missing.attribute', $errorMessage); + $this->assertStringContainsString('[1] => Array', $errorMessage); // This indicates the missing child span + } + } + + /** + * Test that the diff output is generated correctly for multiple root spans. + */ + public function test_trace_structure_diff_output_with_multiple_root_spans(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create first root span + $rootSpan1 = $tracer->spanBuilder('root-span-1') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + $rootSpan1->setAttribute('attribute.one', 'actual-value-1'); + $rootSpan1->end(); + + // Create second root span + $rootSpan2 = $tracer->spanBuilder('root-span-2') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->startSpan(); + $rootSpan2->setAttribute('attribute.two', 42); + $rootSpan2->end(); + + // Define an expected structure that doesn't match the actual structure + $expectedStructure = [ + [ + 'name' => 'root-span-1', + 'kind' => SpanKind::KIND_SERVER, + 'attributes' => [ + 'attribute.one' => 'expected-value-1', // Different value + ], + ], + [ + 'name' => 'root-span-2', + 'kind' => SpanKind::KIND_CLIENT, + 'attributes' => [ + 'attribute.two' => 24, // Different value + 'attribute.three' => true, // Extra attribute + ], + ], + ]; + + try { + // This should fail + $this->assertTraceStructure($this->storage, $expectedStructure); + $this->fail('Expected assertion to fail but it passed'); + } catch (\PHPUnit\Framework\AssertionFailedError $e) { + // Verify that the error message contains the diff + $errorMessage = $e->getMessage(); + + // Check for diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $errorMessage); + $this->assertStringContainsString('+++ Actual Trace Structure', $errorMessage); + + // Check for specific content in the diff for the first root span + $this->assertStringContainsString('root-span-1', $errorMessage); + $this->assertStringContainsString('expected-value-1', $errorMessage); + $this->assertStringContainsString('actual-value-1', $errorMessage); + + // Check for specific content in the diff for the second root span + $this->assertStringContainsString('root-span-2', $errorMessage); + $this->assertStringContainsString('24', $errorMessage); + $this->assertStringContainsString('42', $errorMessage); + $this->assertStringContainsString('attribute.three', $errorMessage); + } + } + + /** + * Test that the diff output is generated correctly when an expected root span is missing. + */ + public function test_trace_structure_diff_output_with_missing_root_span(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create only one root span + $rootSpan = $tracer->spanBuilder('root-span-1') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + $rootSpan->setAttribute('attribute.one', 'value1'); + $rootSpan->end(); + + // Define an expected structure with two root spans + $expectedStructure = [ + [ + 'name' => 'root-span-1', + 'kind' => SpanKind::KIND_SERVER, + 'attributes' => [ + 'attribute.one' => 'value1', + ], + ], + [ + 'name' => 'root-span-2', // This span doesn't exist + 'kind' => SpanKind::KIND_CLIENT, + 'attributes' => [ + 'attribute.two' => 42, + ], + ], + ]; + + try { + // This should fail + $this->assertTraceStructure($this->storage, $expectedStructure); + $this->fail('Expected assertion to fail but it passed'); + } catch (\PHPUnit\Framework\AssertionFailedError $e) { + // Verify that the error message contains the diff + $errorMessage = $e->getMessage(); + + // Check for diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $errorMessage); + $this->assertStringContainsString('+++ Actual Trace Structure', $errorMessage); + + // Check for specific content in the diff + $this->assertStringContainsString('root-span-1', $errorMessage); + + // Check for the missing root span indicator in the diff + $this->assertStringContainsString('[1] => Array', $errorMessage); + + // Check that the error message indicates the missing root span count + $this->assertStringContainsString('Expected 2 root spans', $errorMessage); + $this->assertStringContainsString('found 1', $errorMessage); + } + } + + /** + * Test that the diff output is generated correctly when a nested child span is missing. + */ + public function test_trace_structure_diff_output_with_missing_nested_span(): void + { + $tracer = $this->tracerProvider->getTracer('test-tracer'); + + // Create a root span + $rootSpan = $tracer->spanBuilder('root-span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + // Activate the root span + $rootScope = $rootSpan->activate(); + + try { + // Create only one child span + $childSpan = $tracer->spanBuilder('child-span-1') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->startSpan(); + $childSpan->setAttribute('attribute.one', 'value1'); + $childSpan->end(); + } finally { + $rootSpan->end(); + $rootScope->detach(); + } + + // Define an expected structure with two child spans + $expectedStructure = [ + [ + 'name' => 'root-span', + 'kind' => SpanKind::KIND_SERVER, + 'children' => [ + [ + 'name' => 'child-span-1', + 'kind' => SpanKind::KIND_INTERNAL, + 'attributes' => [ + 'attribute.one' => 'value1', + ], + ], + [ + 'name' => 'child-span-2', // This span doesn't exist + 'kind' => SpanKind::KIND_CLIENT, + 'attributes' => [ + 'attribute.two' => 42, + ], + ], + ], + ], + ]; + + try { + // This should fail + $this->assertTraceStructure($this->storage, $expectedStructure); + $this->fail('Expected assertion to fail but it passed'); + } catch (\PHPUnit\Framework\AssertionFailedError $e) { + // Verify that the error message contains the diff + $errorMessage = $e->getMessage(); + + // Check for diff markers + $this->assertStringContainsString('--- Expected Trace Structure', $errorMessage); + $this->assertStringContainsString('+++ Actual Trace Structure', $errorMessage); + + // Check for specific content in the diff + $this->assertStringContainsString('root-span', $errorMessage); + $this->assertStringContainsString('child-span-1', $errorMessage); + + // Check for the missing child span indicator in the diff + $this->assertStringContainsString('[1] => Array', $errorMessage); + + // Check that the error message indicates a missing span + $this->assertStringContainsString('No matching span found', $errorMessage); + } + } + /** * Test asserting a trace structure using PHPUnit matchers. */