Skip to content

Commit 7a1ae49

Browse files
committed
chore: add ability to use matchers
1 parent 2df9284 commit 7a1ae49

File tree

3 files changed

+343
-37
lines changed

3 files changed

+343
-37
lines changed

src/Utils/Test/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Key features:
2121
- Support for hierarchical span relationships
2222
- Verification of span names, kinds, attributes, events, and status
2323
- Flexible matching with strict and non-strict modes
24+
- Support for PHPUnit matchers/constraints for more flexible assertions
2425
- Detailed error messages for failed assertions
2526

2627
## Requirements
@@ -99,6 +100,66 @@ The `assertTraceStructure` method takes the following parameters:
99100
- `$expectedStructure`: An array defining the expected structure of the trace
100101
- `$strict` (optional): Whether to perform strict matching (all attributes must match)
101102

103+
### Using PHPUnit Matchers
104+
105+
You can use PHPUnit constraints/matchers for more flexible assertions:
106+
107+
```php
108+
use PHPUnit\Framework\Constraint\Callback;
109+
use PHPUnit\Framework\Constraint\IsEqual;
110+
use PHPUnit\Framework\Constraint\IsIdentical;
111+
use PHPUnit\Framework\Constraint\IsType;
112+
use PHPUnit\Framework\Constraint\RegularExpression;
113+
use PHPUnit\Framework\Constraint\StringContains;
114+
115+
// Define the expected structure with matchers
116+
$expectedStructure = [
117+
[
118+
'name' => 'root-span',
119+
'kind' => new IsIdentical(SpanKind::KIND_SERVER),
120+
'attributes' => [
121+
'string.attribute' => new StringContains('World'),
122+
'numeric.attribute' => new Callback(function ($value) {
123+
return $value > 40 || $value === 42;
124+
}),
125+
'boolean.attribute' => new IsType('boolean'),
126+
'array.attribute' => new Callback(function ($value) {
127+
return is_array($value) && count($value) === 3 && in_array('b', $value);
128+
}),
129+
],
130+
'children' => [
131+
[
132+
'name' => new RegularExpression('/child-span-\d+/'),
133+
'kind' => SpanKind::KIND_INTERNAL,
134+
'attributes' => [
135+
'timestamp' => new IsType('integer'),
136+
],
137+
'events' => [
138+
[
139+
'name' => 'process.start',
140+
'attributes' => [
141+
'process.id' => new IsType('integer'),
142+
'process.name' => new StringContains('process'),
143+
],
144+
],
145+
],
146+
],
147+
],
148+
],
149+
];
150+
151+
// Assert the trace structure with matchers
152+
$this->assertTraceStructure($spans, $expectedStructure);
153+
```
154+
155+
Supported PHPUnit matchers include:
156+
- `StringContains` for partial string matching
157+
- `RegularExpression` for pattern matching
158+
- `IsIdentical` for strict equality
159+
- `IsEqual` for loose equality
160+
- `IsType` for type checking
161+
- `Callback` for custom validation logic
162+
102163
## Installation via composer
103164

104165
```bash

src/Utils/Test/src/TraceStructureAssertionTrait.php

Lines changed: 156 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use OpenTelemetry\SDK\Trace\ImmutableSpan;
99
use PHPUnit\Framework\Assert;
1010
use PHPUnit\Framework\AssertionFailedError;
11+
use PHPUnit\Framework\Constraint\Constraint;
1112
use Traversable;
1213

1314
/**
@@ -225,20 +226,50 @@ private function findMatchingSpan(array $expectedSpan, array $actualSpans, bool
225226
throw new \InvalidArgumentException('Expected span must have a name');
226227
}
227228

228-
// Find a span with the matching name
229-
$matchingSpans = array_filter($actualSpans, function ($actualSpan) use ($expectedName) {
230-
return $actualSpan['name'] === $expectedName;
231-
});
229+
// Check if the expected name is a constraint
230+
if ($this->isConstraint($expectedName)) {
231+
// Find spans that match the constraint
232+
$matchingSpans = [];
233+
foreach ($actualSpans as $actualSpan) {
234+
try {
235+
Assert::assertThat(
236+
$actualSpan['name'],
237+
$expectedName,
238+
'Span name does not match constraint'
239+
);
240+
$matchingSpans[] = $actualSpan;
241+
} catch (AssertionFailedError $e) {
242+
// This span doesn't match the constraint, skip it
243+
continue;
244+
}
245+
}
246+
} else {
247+
// Find spans with the exact matching name
248+
$matchingSpans = array_filter($actualSpans, function ($actualSpan) use ($expectedName) {
249+
return $actualSpan['name'] === $expectedName;
250+
});
251+
}
232252

233253
Assert::assertNotEmpty(
234254
$matchingSpans,
235-
sprintf('No span with name "%s" found', $expectedName)
255+
sprintf(
256+
'No span matching name "%s" found',
257+
$this->isConstraint($expectedName) ? 'constraint' : $expectedName
258+
)
236259
);
237260

238-
// If multiple spans have the same name, try to match based on other properties
261+
// If multiple spans match, try to match based on other properties
239262
foreach ($matchingSpans as $actualSpan) {
240263
try {
241-
$this->compareSpans($expectedSpan, $actualSpan, $strict);
264+
// For constraint-based names, we need to modify the expected span for comparison
265+
// since compareSpans expects exact name matching
266+
$spanToCompare = $expectedSpan;
267+
if ($this->isConstraint($expectedName)) {
268+
$spanToCompare = $expectedSpan;
269+
$spanToCompare['name'] = $actualSpan['name'];
270+
}
271+
272+
$this->compareSpans($spanToCompare, $actualSpan, $strict);
242273

243274
// If we get here, the spans match
244275
// Now check children if they exist
@@ -256,7 +287,10 @@ private function findMatchingSpan(array $expectedSpan, array $actualSpans, bool
256287

257288
// If we get here, none of the spans matched
258289
Assert::fail(
259-
sprintf('No matching span found for expected span "%s"', $expectedName)
290+
sprintf(
291+
'No matching span found for expected span "%s"',
292+
$this->isConstraint($expectedName) ? 'constraint' : $expectedName
293+
)
260294
);
261295
}
262296

@@ -280,11 +314,21 @@ private function compareSpans(array $expectedSpan, array $actualSpan, bool $stri
280314

281315
// Compare kind if specified
282316
if (isset($expectedSpan['kind'])) {
283-
Assert::assertSame(
284-
$expectedSpan['kind'],
285-
$actualSpan['kind'],
286-
sprintf('Span kinds do not match for span "%s"', $expectedSpan['name'])
287-
);
317+
$expectedKind = $expectedSpan['kind'];
318+
319+
if ($this->isConstraint($expectedKind)) {
320+
Assert::assertThat(
321+
$actualSpan['kind'],
322+
$expectedKind,
323+
sprintf('Span kind does not match constraint for span "%s"', $expectedSpan['name'])
324+
);
325+
} else {
326+
Assert::assertSame(
327+
$expectedKind,
328+
$actualSpan['kind'],
329+
sprintf('Span kinds do not match for span "%s"', $expectedSpan['name'])
330+
);
331+
}
288332
}
289333

290334
// Compare attributes if specified
@@ -345,6 +389,17 @@ private function compareChildren(array $expectedChildren, array $actualChildren,
345389
}
346390
}
347391

392+
/**
393+
* Checks if a value is a PHPUnit constraint object.
394+
*
395+
* @param mixed $value
396+
* @return bool
397+
*/
398+
private function isConstraint($value): bool
399+
{
400+
return $value instanceof Constraint;
401+
}
402+
348403
/**
349404
* Compares the attributes of an expected span with the attributes of an actual span.
350405
*
@@ -370,12 +425,25 @@ private function compareAttributes(array $expectedAttributes, array $actualAttri
370425
continue;
371426
}
372427

373-
// Compare the attribute values
374-
Assert::assertEquals(
375-
$expectedValue,
376-
$actualAttributes[$key],
377-
sprintf('Attribute "%s" value does not match in span "%s"', $key, $spanName)
378-
);
428+
// Get the actual value
429+
$actualValue = $actualAttributes[$key];
430+
431+
// Check if the expected value is a constraint
432+
if ($this->isConstraint($expectedValue)) {
433+
// Use assertThat for constraint evaluation
434+
Assert::assertThat(
435+
$actualValue,
436+
$expectedValue,
437+
sprintf('Attribute "%s" value does not match constraint in span "%s"', $key, $spanName)
438+
);
439+
} else {
440+
// Use regular assertEquals for direct comparison
441+
Assert::assertEquals(
442+
$expectedValue,
443+
$actualValue,
444+
sprintf('Attribute "%s" value does not match in span "%s"', $key, $spanName)
445+
);
446+
}
379447
}
380448
}
381449

@@ -392,20 +460,40 @@ private function compareStatus(array $expectedStatus, array $actualStatus, strin
392460
{
393461
// Compare status code if specified
394462
if (isset($expectedStatus['code'])) {
395-
Assert::assertSame(
396-
$expectedStatus['code'],
397-
$actualStatus['code'],
398-
sprintf('Status code does not match for span "%s"', $spanName)
399-
);
463+
$expectedCode = $expectedStatus['code'];
464+
465+
if ($this->isConstraint($expectedCode)) {
466+
Assert::assertThat(
467+
$actualStatus['code'],
468+
$expectedCode,
469+
sprintf('Status code does not match constraint for span "%s"', $spanName)
470+
);
471+
} else {
472+
Assert::assertSame(
473+
$expectedCode,
474+
$actualStatus['code'],
475+
sprintf('Status code does not match for span "%s"', $spanName)
476+
);
477+
}
400478
}
401479

402480
// Compare status description if specified
403481
if (isset($expectedStatus['description'])) {
404-
Assert::assertSame(
405-
$expectedStatus['description'],
406-
$actualStatus['description'],
407-
sprintf('Status description does not match for span "%s"', $spanName)
408-
);
482+
$expectedDescription = $expectedStatus['description'];
483+
484+
if ($this->isConstraint($expectedDescription)) {
485+
Assert::assertThat(
486+
$actualStatus['description'],
487+
$expectedDescription,
488+
sprintf('Status description does not match constraint for span "%s"', $spanName)
489+
);
490+
} else {
491+
Assert::assertSame(
492+
$expectedDescription,
493+
$actualStatus['description'],
494+
sprintf('Status description does not match for span "%s"', $spanName)
495+
);
496+
}
409497
}
410498
}
411499

@@ -471,17 +559,40 @@ private function findMatchingEvent(array $expectedEvent, array $actualEvents, bo
471559
throw new \InvalidArgumentException('Expected event must have a name');
472560
}
473561

474-
// Find an event with the matching name
475-
$matchingEvents = array_filter($actualEvents, function ($actualEvent) use ($expectedName) {
476-
return $actualEvent['name'] === $expectedName;
477-
});
562+
// Check if the expected name is a constraint
563+
if ($this->isConstraint($expectedName)) {
564+
// Find events that match the constraint
565+
$matchingEvents = [];
566+
foreach ($actualEvents as $actualEvent) {
567+
try {
568+
Assert::assertThat(
569+
$actualEvent['name'],
570+
$expectedName,
571+
'Event name does not match constraint'
572+
);
573+
$matchingEvents[] = $actualEvent;
574+
} catch (AssertionFailedError $e) {
575+
// This event doesn't match the constraint, skip it
576+
continue;
577+
}
578+
}
579+
} else {
580+
// Find events with the exact matching name
581+
$matchingEvents = array_filter($actualEvents, function ($actualEvent) use ($expectedName) {
582+
return $actualEvent['name'] === $expectedName;
583+
});
584+
}
478585

479586
Assert::assertNotEmpty(
480587
$matchingEvents,
481-
sprintf('No event with name "%s" found in span "%s"', $expectedName, $spanName)
588+
sprintf(
589+
'No event matching name "%s" found in span "%s"',
590+
$this->isConstraint($expectedName) ? 'constraint' : $expectedName,
591+
$spanName
592+
)
482593
);
483594

484-
// If multiple events have the same name, try to match based on attributes
595+
// If multiple events match, try to match based on attributes
485596
foreach ($matchingEvents as $actualEvent) {
486597
try {
487598
// Compare attributes if specified
@@ -490,7 +601,11 @@ private function findMatchingEvent(array $expectedEvent, array $actualEvents, bo
490601
$expectedEvent['attributes'],
491602
$actualEvent['attributes'],
492603
$strict,
493-
sprintf('Event "%s" in span "%s"', $expectedName, $spanName)
604+
sprintf(
605+
'Event "%s" in span "%s"',
606+
$this->isConstraint($expectedName) ? $actualEvent['name'] : $expectedName,
607+
$spanName
608+
)
494609
);
495610
}
496611

@@ -504,7 +619,11 @@ private function findMatchingEvent(array $expectedEvent, array $actualEvents, bo
504619

505620
// If we get here, none of the events matched
506621
Assert::fail(
507-
sprintf('No matching event found for expected event "%s" in span "%s"', $expectedName, $spanName)
622+
sprintf(
623+
'No matching event found for expected event "%s" in span "%s"',
624+
$this->isConstraint($expectedName) ? 'constraint' : $expectedName,
625+
$spanName
626+
)
508627
);
509628
}
510629
}

0 commit comments

Comments
 (0)