Skip to content

Commit 930900c

Browse files
authored
feat: add rich diff by default (open-telemetry#346)
* feat: add rich diff by default * chore: add test covering multiple root spans * chore: test more multiple root spans * chore: test missing nested span diff * fix: dont use internal classes * fix: avoid possible type coercion * chore: coverage * fix: add #Override
1 parent 26ef1c2 commit 930900c

File tree

7 files changed

+1765
-155
lines changed

7 files changed

+1765
-155
lines changed

src/Utils/Test/src/Fluent/TraceAssertionFailedException.php

Lines changed: 181 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -60,171 +60,187 @@ public function getActualStructure(): array
6060
*/
6161
private function formatDiff(array $expected, array $actual): string
6262
{
63-
$output = "\n\nExpected Trace Structure:\n";
64-
$output .= $this->formatExpectedStructure($expected);
63+
// First, convert the fluent assertion structures to a format suitable for diffing
64+
$expectedStructure = $this->convertFluentStructureToArray($expected);
65+
$actualStructure = $this->convertFluentStructureToArray($actual);
6566

66-
$output .= "\n\nActual Trace Structure:\n";
67-
$output .= $this->formatActualStructure($actual);
67+
// Generate a PHPUnit-style diff
68+
$output = "\n\n--- Expected Trace Structure\n";
69+
$output .= "+++ Actual Trace Structure\n";
70+
$output .= "@@ @@\n";
71+
72+
// Generate the diff for the root level
73+
$output .= $this->generateArrayDiff($expectedStructure, $actualStructure);
6874

6975
return $output;
7076
}
7177

7278
/**
73-
* Format the expected structure.
79+
* Converts the fluent assertion structure to a format suitable for diffing.
7480
*
75-
* @param array $expected The expected structure
76-
* @param int $indent The indentation level
77-
* @return string
81+
* @param array $structure The fluent assertion structure
82+
* @return array The converted structure
7883
*/
79-
private function formatExpectedStructure(array $expected, int $indent = 0): string
84+
private function convertFluentStructureToArray(array $structure): array
8085
{
81-
$output = '';
82-
$indentation = str_repeat(' ', $indent);
86+
$result = [];
8387

84-
foreach ($expected as $item) {
88+
foreach ($structure as $item) {
8589
if (!isset($item['type'])) {
8690
continue;
8791
}
8892

8993
switch ($item['type']) {
9094
case 'root_span':
91-
$output .= $indentation . "Root Span: \"{$item['name']}\"\n";
95+
$result[] = [
96+
'name' => $item['name'],
97+
'type' => 'root',
98+
];
9299

93100
break;
94-
case 'root_span_count':
95-
$output .= $indentation . "Root Span Count: {$item['count']}\n";
96101

97-
break;
98-
case 'span_kind':
99-
$output .= $indentation . 'Kind: ' . $this->formatKind($item['kind']) . "\n";
102+
case 'child_span':
103+
$result[] = [
104+
'name' => $item['name'],
105+
'type' => 'child',
106+
];
100107

101108
break;
102-
case 'span_attribute':
103-
$output .= $indentation . "Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n";
104109

105-
break;
106-
case 'span_status':
107-
$output .= $indentation . 'Status: Code=' . $this->formatValue($item['code']);
108-
if (isset($item['description'])) {
109-
$output .= ", Description=\"{$item['description']}\"";
110-
}
111-
$output .= "\n";
110+
case 'missing_root_span':
111+
$result[] = [
112+
'name' => $item['expected_name'],
113+
'type' => 'root',
114+
'missing' => true,
115+
'available' => $item['available_root_spans'] ?? [],
116+
];
112117

113118
break;
114-
case 'span_event':
115-
$output .= $indentation . "Event: \"{$item['name']}\"\n";
116119

117-
break;
118-
case 'span_event_attribute':
119-
$output .= $indentation . "Event Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n";
120+
case 'missing_child_span':
121+
$result[] = [
122+
'name' => $item['expected_name'],
123+
'type' => 'child',
124+
'missing' => true,
125+
'available' => $item['available_spans'] ?? [],
126+
];
120127

121128
break;
122-
case 'child_span':
123-
$output .= $indentation . "Child Span: \"{$item['name']}\"\n";
124129

125-
break;
126-
case 'child_span_count':
127-
$output .= $indentation . "Child Span Count: {$item['count']}\n";
130+
case 'root_span_count':
131+
$result[] = [
132+
'type' => 'count',
133+
'count' => $item['count'],
134+
'spans' => $item['spans'] ?? [],
135+
];
128136

129137
break;
138+
139+
// Add other types as needed
130140
}
131141
}
132142

133-
return $output;
143+
return $result;
134144
}
135145

136146
/**
137-
* Format the actual structure.
147+
* Recursively generates a diff between two arrays.
138148
*
139-
* @param array $actual The actual structure
140-
* @param int $indent The indentation level
141-
* @return string
149+
* @param array $expected The expected array
150+
* @param array $actual The actual array
151+
* @param int $depth The current depth for indentation
152+
* @return string The formatted diff
142153
*/
143-
private function formatActualStructure(array $actual, int $indent = 0): string
154+
private function generateArrayDiff(array $expected, array $actual, int $depth = 0): string
144155
{
145156
$output = '';
146-
$indentation = str_repeat(' ', $indent);
147-
148-
foreach ($actual as $item) {
149-
if (!isset($item['type'])) {
150-
continue;
151-
}
152-
153-
switch ($item['type']) {
154-
case 'root_span':
155-
$output .= $indentation . "Root Span: \"{$item['name']}\"\n";
156-
157-
break;
158-
case 'missing_root_span':
159-
$output .= $indentation . "Missing Root Span: \"{$item['expected_name']}\"\n";
160-
if (!empty($item['available_root_spans'])) {
161-
$output .= $indentation . ' Available Root Spans: ' . implode(', ', array_map(function ($name) {
162-
return "\"$name\"";
163-
}, $item['available_root_spans'])) . "\n";
164-
}
165-
166-
break;
167-
case 'root_span_count':
168-
$output .= $indentation . "Root Span Count: {$item['count']}\n";
169-
if (!empty($item['spans'])) {
170-
$output .= $indentation . ' Root Spans: ' . implode(', ', array_map(function ($name) {
171-
return "\"$name\"";
172-
}, $item['spans'])) . "\n";
157+
$indent = str_repeat(' ', $depth);
158+
159+
// If arrays are indexed numerically, compare them as lists
160+
if ($this->isIndexedArray($expected) && $this->isIndexedArray($actual)) {
161+
$output .= $indent . "Array (\n";
162+
163+
// Find the maximum index to iterate through
164+
$maxIndex = max(count($expected), count($actual)) - 1;
165+
166+
for ($i = 0; $i <= $maxIndex; $i++) {
167+
if (isset($expected[$i]) && isset($actual[$i])) {
168+
// Both arrays have this index, compare the values
169+
if (is_array($expected[$i]) && is_array($actual[$i])) {
170+
// Both values are arrays, recursively compare
171+
$output .= $indent . " [$i] => Array (\n";
172+
$output .= $this->generateArrayDiff($expected[$i], $actual[$i], $depth + 2);
173+
$output .= $indent . " )\n";
174+
} elseif ($expected[$i] === $actual[$i]) {
175+
// Values are the same
176+
$output .= $indent . " [$i] => " . $this->formatValue($expected[$i]) . "\n";
177+
} else {
178+
// Values are different
179+
$output .= $indent . "- [$i] => " . $this->formatValue($expected[$i]) . "\n";
180+
$output .= $indent . "+ [$i] => " . $this->formatValue($actual[$i]) . "\n";
173181
}
182+
} elseif (isset($expected[$i])) {
183+
// Only in expected
184+
$output .= $indent . "- [$i] => " . $this->formatValue($expected[$i]) . "\n";
185+
} else {
186+
// Only in actual
187+
$output .= $indent . "+ [$i] => " . $this->formatValue($actual[$i]) . "\n";
188+
}
189+
}
174190

175-
break;
176-
case 'span_kind':
177-
$output .= $indentation . 'Kind: ' . $this->formatKind($item['kind']) . "\n";
178-
179-
break;
180-
case 'span_attribute':
181-
$output .= $indentation . "Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n";
182-
183-
break;
184-
case 'missing_span_attribute':
185-
$output .= $indentation . "Missing Attribute: \"{$item['key']}\"\n";
186-
187-
break;
188-
case 'span_status':
189-
$output .= $indentation . 'Status: Code=' . $this->formatValue($item['code']);
190-
if (isset($item['description'])) {
191-
$output .= ", Description=\"{$item['description']}\"";
191+
$output .= $indent . ")\n";
192+
} else {
193+
// Compare as associative arrays
194+
$output .= $indent . "Array (\n";
195+
196+
// Get all keys from both arrays
197+
$allKeys = array_unique(array_merge(array_keys($expected), array_keys($actual)));
198+
sort($allKeys);
199+
200+
foreach ($allKeys as $key) {
201+
if (isset($expected[$key]) && isset($actual[$key])) {
202+
// Both arrays have this key, compare the values
203+
if (is_array($expected[$key]) && is_array($actual[$key])) {
204+
// Both values are arrays, recursively compare
205+
$output .= $indent . " ['$key'] => Array (\n";
206+
$output .= $this->generateArrayDiff($expected[$key], $actual[$key], $depth + 2);
207+
$output .= $indent . " )\n";
208+
} elseif ($expected[$key] === $actual[$key]) {
209+
// Values are the same
210+
$output .= $indent . " ['$key'] => " . $this->formatValue($expected[$key]) . "\n";
211+
} else {
212+
// Values are different
213+
$output .= $indent . "- ['$key'] => " . $this->formatValue($expected[$key]) . "\n";
214+
$output .= $indent . "+ ['$key'] => " . $this->formatValue($actual[$key]) . "\n";
192215
}
193-
$output .= "\n";
194-
195-
break;
196-
case 'span_event':
197-
$output .= $indentation . "Event: \"{$item['name']}\"\n";
198-
199-
break;
200-
case 'missing_span_event':
201-
$output .= $indentation . "Missing Event: \"{$item['expected_name']}\"\n";
202-
203-
break;
204-
case 'span_event_attribute':
205-
$output .= $indentation . "Event Attribute \"{$item['key']}\": " . $this->formatValue($item['value']) . "\n";
206-
207-
break;
208-
case 'child_span':
209-
$output .= $indentation . "Child Span: \"{$item['name']}\"\n";
210-
211-
break;
212-
case 'missing_child_span':
213-
$output .= $indentation . "Missing Child Span: \"{$item['expected_name']}\"\n";
216+
} elseif (isset($expected[$key])) {
217+
// Only in expected
218+
$output .= $indent . "- ['$key'] => " . $this->formatValue($expected[$key]) . "\n";
219+
} else {
220+
// Only in actual
221+
$output .= $indent . "+ ['$key'] => " . $this->formatValue($actual[$key]) . "\n";
222+
}
223+
}
214224

215-
break;
216-
case 'unexpected_child_span':
217-
$output .= $indentation . "Unexpected Child Span: \"{$item['name']}\"\n";
225+
$output .= $indent . ")\n";
226+
}
218227

219-
break;
220-
case 'child_span_count':
221-
$output .= $indentation . "Child Span Count: {$item['count']}\n";
228+
return $output;
229+
}
222230

223-
break;
224-
}
231+
/**
232+
* Checks if an array is indexed numerically (not associative).
233+
*
234+
* @param array $array The array to check
235+
* @return bool True if the array is indexed, false if it's associative
236+
*/
237+
private function isIndexedArray(array $array): bool
238+
{
239+
if (empty($array)) {
240+
return true;
225241
}
226242

227-
return $output;
243+
return array_keys($array) === range(0, count($array) - 1);
228244
}
229245

230246
/**
@@ -242,14 +258,32 @@ private function formatValue($value): string
242258
} elseif (null === $value) {
243259
return 'null';
244260
} elseif (is_array($value)) {
261+
// Check if this array looks like a status
262+
if (isset($value['code']) || isset($value['description'])) {
263+
return $this->formatStatus($value);
264+
}
265+
245266
$json = json_encode($value);
246267

247268
return $json === false ? '[unable to encode]' : $json;
269+
} elseif (is_int($value) && $this->isSpanKind($value)) {
270+
return $this->formatKind($value);
248271
}
249272

250273
return (string) $value;
251274
}
252275

276+
/**
277+
* Checks if a value is a span kind.
278+
*
279+
* @param int $value The value to check
280+
* @return bool True if the value is a span kind
281+
*/
282+
private function isSpanKind(int $value): bool
283+
{
284+
return $value >= 0 && $value <= 4;
285+
}
286+
253287
/**
254288
* Format a span kind for display.
255289
*
@@ -268,4 +302,34 @@ private function formatKind(int $kind): string
268302

269303
return $kinds[$kind] ?? "UNKNOWN_KIND($kind)";
270304
}
305+
306+
/**
307+
* Format a span status for display.
308+
*
309+
* @param array $status The span status
310+
* @return string
311+
*/
312+
private function formatStatus(array $status): string
313+
{
314+
$output = 'Status: Code=';
315+
316+
if (isset($status['code'])) {
317+
$statusCodes = [
318+
0 => 'STATUS_UNSET',
319+
1 => 'STATUS_OK',
320+
2 => 'STATUS_ERROR',
321+
];
322+
323+
$code = $status['code'];
324+
$output .= isset($statusCodes[$code]) ? $statusCodes[$code] : "UNKNOWN_STATUS($code)";
325+
} else {
326+
$output .= 'UNDEFINED';
327+
}
328+
329+
if (isset($status['description']) && $status['description']) {
330+
$output .= ", Description=\"{$status['description']}\"";
331+
}
332+
333+
return $output;
334+
}
271335
}

0 commit comments

Comments
 (0)