Commit 44c32205 authored by Mickaël Bourgier's avatar Mickaël Bourgier
Browse files

🐛 Only stores ClassStub of projections in data collector

Storing the whole projection was causing problems when it wasn't serializable.
Events stored in data collector is now an array of plain arrays with only necessary data, and events themselves are now cloned.
Test of html rendering has been updated to use real class data collector instead of a fake one.
parent 91a1e461
......@@ -10,9 +10,12 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Cloner\Data;
use Throwable;
use Webf\Projections\Core\Event\EventClassManagerInterface;
use Webf\Projections\Core\Event\EventInterface;
use Webf\Projections\Core\Service\Data\Caller;
use Webf\Projections\Core\Service\Data\DispatchedEvent;
use Webf\Projections\Core\Service\Data\ProcessedProjection;
use Webf\Projections\Core\Service\EventProjectorInterface;
use Webf\Projections\Core\Service\ProjectionManagerInterface;
use Webf\Projections\Core\Service\TraceableEventProjector;
......@@ -32,7 +35,20 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
* nb_projections?: int,
* nb_total_projections?: int,
* execution_time?: float,
* events?: array<DispatchedEvent>
* events?: array<array{
* type: Data,
* execution_time: float,
* failed: bool,
* orphaned: bool,
* caller: Caller|null,
* value: Data,
* projections: array<array{
* type: Data,
* execution_time: float,
* failed: bool,
* throwable: Data
* }>
* }>
* }
*/
protected $data = [];
......@@ -52,7 +68,7 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
public function collect(
Request $request,
Response $response,
\Throwable $exception = null
Throwable $exception = null
): void {
// See $this->lateCollect()
}
......@@ -62,6 +78,19 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
return self::NAME;
}
/**
* @codeCoverageIgnore
*/
protected function getCasters(): array
{
$casters = parent::getCasters();
// Unset the default caster truncating collectors data.
unset($casters['*']);
return $casters;
}
public function reset(): void
{
$this->data = [];
......@@ -81,7 +110,7 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
= $nbOrphanedEvents
= $executionTime = 0;
$eventTypes = $projectionTypes = [];
$eventTypes = $projections = [];
foreach ($events as $event) {
++$nbEvents;
......@@ -98,7 +127,7 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
$eventTypes[get_class($event->getValue())] = null;
foreach ($event->getProjections() as $projection) {
$projectionTypes[get_class($projection->getValue())] = null;
$projections[spl_object_hash($projection->getValue())] = null;
}
}
......@@ -106,7 +135,7 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
EventInterface::class,
]);
$projections = $this->projectionManager->getProjections();
$allProjections = $this->projectionManager->getProjections();
$this->data = [
'nb_events' => $nbEvents,
......@@ -115,10 +144,31 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
'nb_orphaned_events' => $nbOrphanedEvents,
'nb_event_types' => count($eventTypes),
'nb_total_event_types' => count($eventClasses),
'nb_projections' => count($projectionTypes),
'nb_total_projections' => count($projections),
'nb_projections' => count($projections),
'nb_total_projections' => count($allProjections),
'execution_time' => $executionTime,
'events' => $events,
'events' => array_map(
fn (DispatchedEvent $dispatchedEvent) => [
'type' => $this->getType($dispatchedEvent->getValue()),
'execution_time' => $dispatchedEvent->getExecutionTime(),
'failed' => $dispatchedEvent->hasFailed(),
'orphaned' => 0 === count($dispatchedEvent->getProjections()),
'caller' => $dispatchedEvent->getCaller(),
'value' => $this->cloneVar($dispatchedEvent->getValue()),
'projections' => array_map(
fn (ProcessedProjection $processedProjection) => [
'type' => $this->getType($processedProjection->getValue()),
'execution_time' => $processedProjection->getExecutionTime(),
'failed' => $processedProjection->hasFailed(),
'throwable' => $this->cloneVar(
$processedProjection->getThrowable()
),
],
$dispatchedEvent->getProjections()
),
],
$events
),
];
}
......@@ -168,17 +218,30 @@ class WebfProjectionsDataCollector extends DataCollector implements LateDataColl
}
/**
* @return DispatchedEvent[]
* @psalm-return array<array{
* type: Data,
* execution_time: float,
* failed: bool,
* orphaned: bool,
* caller: Caller|null,
* value: Data,
* projections: array<array{
* type: Data,
* execution_time: float,
* failed: bool,
* throwable: Data
* }>
* }>
*/
public function getEvents(): array
{
return $this->data['events'] ?? [];
}
public function getClassStub(string $className): Data
private function getType(object $object): Data
{
/** @var Data[] $data */
$data = $this->cloneVar([new ClassStub($className)]);
$data = $this->cloneVar([new ClassStub(get_class($object))]);
return $data[0];
}
......
......@@ -160,7 +160,7 @@
{{ helper.render_event_table(
event,
event.failed
or event.projections | length == 0
or event.orphaned
or collector.nbFailedEvents == 0
and collector.nbOrphanedEvents == 0
and loop.index0 == 0,
......@@ -250,7 +250,7 @@
{% if collector.nbOrphanedEvents > 0 %}
{% for event in collector.events %}
{% set first_seen = false %}
{% if event.projections | length == 0 %}
{% if event.orphaned %}
{{ helper.render_event_table(
event,
true,
......@@ -279,7 +279,6 @@
{% macro render_event_table(event, open = false, collector) %}
{% import _self as helper %}
{# @var event \Webf\Projections\Core\Service\Data\DispatchedEvent #}
{% set discr = random() %}
<table class="dispatched-event-item">
<thead>
......@@ -289,15 +288,15 @@
data-toggle-initial="{{ open ? 'display' }}"
>
<span class="dump-inline">
{{ profiler_dump(collector.classStub(event.type)) }}
{{ profiler_dump(event.type) }}
</span>
<span class="text-muted execution-time">
({{ '%.2F' | format(event.executionTime) }} ms)
({{ '%.2F' | format(event.execution_time) }} ms)
</span>
{% if event.failed %}
<span class="label status-error failed">failed</span>
{% endif %}
{% if event.projections | length == 0 %}
{% if event.orphaned %}
<span class="label status-warning orphaned">orphaned</span>
{% endif %}
<a class="toggle-button">
......@@ -313,7 +312,6 @@
</thead>
<tbody id="dispatched-event-item-{{ discr }}-details" class="sf-toggle-content">
{# @var \Webf\Projections\Core\Service\Data\Caller caller #}
{% set caller = event.caller %}
{% if caller %}
<tr>
......@@ -349,7 +347,7 @@
<tr>
<td class="text-bold">Event</td>
<td>{{ dump(event.value) }}</td>
<td>{{ profiler_dump(event.value) }}</td>
</tr>
<tr class="projections">
......@@ -380,16 +378,14 @@
{% endmacro %}
{% macro render_projection_td(event, projection, collector) %}
{# @var \Webf\Projections\Core\Service\Data\DispatchedEvent event #}
{# @var \Webf\Projections\Core\Service\Data\ProcessedProjection projection #}
<td class="projection {{ projection.hasFailed() ? 'status-error' }}">
<span class="dump-inline">{{ profiler_dump(collector.classStub(projection.type)) }}</span>
<td class="projection {{ projection.failed ? 'status-error' }}">
<span class="dump-inline">{{ profiler_dump(projection.type) }}</span>
<span class="text-muted">
({{ '%.2F' | format(projection.executionTime) }} ms)
({{ '%.2F' | format(projection.execution_time) }} ms)
</span>
{% if projection.hasFailed() %}
{% if projection.failed %}
<span class="font-normal label status-error">failed</span>
{{ dump(projection.throwable) }}
{{ profiler_dump(projection.throwable) }}
{% endif %}
</td>
{% endmacro %}
......@@ -7,11 +7,11 @@ namespace Tests\DataCollector;
use Exception;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Tests\Event\Stub\EventStub;
use Tests\Event\Stub\EventStubA;
use Tests\Event\Stub\EventStubB;
use Tests\Projection\Stub\ProjectionStub;
use Tests\Projection\Stub\UnserializableProjection;
use Tests\Service\Stub\FakeTraceableEventProjector;
use Webf\Projections\Bundle\DataCollector\WebfProjectionsDataCollector;
use Webf\Projections\Core\Event\EventClassManagerInterface;
......@@ -70,14 +70,14 @@ class WebfProjectionsDataCollectorTest extends TestCase
public function test_late_collect(): void
{
$e1 = new DispatchedEvent(new EventStubA(), null);
$e1->projectionStarted(new ProjectionStub());
$e1->projectionStarted($projections[] = new ProjectionStub());
$e1->projectionSucceeded();
$e1->projectionStarted(new ProjectionStub());
$e1->projectionStarted($projections[] = new ProjectionStub());
$e1->projectionSucceeded();
$e1->dispatched();
$e2 = new DispatchedEvent(new EventStubA(), null);
$e2->projectionStarted(new ProjectionStub());
$e2->projectionStarted($projections[] = new ProjectionStub());
$e2->projectionFailed(new Exception());
$e2->dispatched();
......@@ -85,13 +85,13 @@ class WebfProjectionsDataCollectorTest extends TestCase
$e3->dispatched();
$eventClasses = [EventStub::class, EventStubA::class, EventStubB::class];
$projections = [new ProjectionStub(), new ProjectionStub()];
$allProjections = [new ProjectionStub(), new ProjectionStub()];
$dataCollector = new WebfProjectionsDataCollector(
new InMemoryEventClassManager($eventClasses),
new FakeTraceableEventProjector([$e1, $e2, $e3]),
new ProjectionManager([
new ProjectionFactory($projections),
new ProjectionFactory($allProjections),
])
);
......@@ -106,13 +106,57 @@ class WebfProjectionsDataCollectorTest extends TestCase
count($eventClasses),
$dataCollector->getNbTotalEventTypes()
);
$this->assertSame(1, $dataCollector->getNbProjections());
$this->assertSame(count($projections), $dataCollector->getNbTotalProjections());
$this->assertSame(count($projections), $dataCollector->getNbProjections());
$this->assertSame(count($allProjections), $dataCollector->getNbTotalProjections());
$this->assertSame(
$e1->getExecutionTime() + $e2->getExecutionTime() + $e3->getExecutionTime(),
$dataCollector->getExecutionTime()
);
$this->assertSame([$e1, $e2, $e3], $dataCollector->getEvents());
$events = $dataCollector->getEvents();
foreach ([$e1, $e2, $e3] as $i => $event) {
$this->assertEquals(
get_class($event->getValue()),
(string) $events[$i]['type']
);
$this->assertEquals(
$event->getExecutionTime(),
$events[$i]['execution_time']
);
$this->assertEquals(
$event->hasFailed(),
$events[$i]['failed']
);
$this->assertEquals(
0 === count($event->getProjections()),
$events[$i]['orphaned']
);
$this->assertEquals(
$event->getCaller(),
$events[$i]['caller']
);
$this->assertEquals(
get_class($event->getValue()),
$events[$i]['value']->getType()
);
foreach ($event->getProjections() as $j => $projection) {
$this->assertEquals(
get_class($projection->getValue()),
(string) $events[$i]['projections'][$j]['type']
);
$this->assertEquals(
$projection->getExecutionTime(),
$events[$i]['projections'][$j]['execution_time']
);
$this->assertEquals(
$projection->hasFailed(),
$events[$i]['projections'][$j]['failed']
);
$this->assertEquals(
is_object($t = $projection->getThrowable()) ? get_class($t) : gettype($t),
$events[$i]['projections'][$j]['throwable']->getType()
);
}
}
}
public function test_late_collect_need_traceable_event_projector()
......@@ -166,19 +210,23 @@ class WebfProjectionsDataCollectorTest extends TestCase
$this->assertEquals(0, $dataCollector->getNbSuccessfulEvents());
}
public function test_get_class_stub()
public function test_projection_can_be_unserializable()
{
$dataCollector = new WebfProjectionsDataCollector(
$this->createMock(EventClassManagerInterface::class),
$this->createMock(EventProjectorInterface::class),
$this->createMock(ProjectionManagerInterface::class)
new InMemoryEventClassManager([EventStub::class]),
$eventProjector = new TraceableEventProjector(
$projectionManager = new ProjectionManager([
new ProjectionFactory([
new UnserializableProjection([EventStub::class]),
]),
])
),
$projectionManager
);
$classStubData = $dataCollector->getClassStub(WebfProjectionsDataCollector::class);
$eventProjector->dispatch(new EventStub());
$dataCollector->lateCollect();
$this->assertEquals(
new ClassStub(WebfProjectionsDataCollector::class),
$classStubData->getValue()
);
$this->assertNotEmpty(serialize($dataCollector));
}
}
<?php
declare(strict_types=1);
namespace Tests\Projection\Stub;
use Closure;
use Webf\Projections\Core\ProjectionStorage\InMemoryProjectionStorage;
class UnserializableProjection extends ProjectionStub
{
private Closure $unserializableValue;
public function __construct(array $subscribedEvents = [], ?InMemoryProjectionStorage $storage = null)
{
parent::__construct($subscribedEvents, $storage);
$this->unserializableValue = function () {};
}
}
......@@ -8,15 +8,20 @@ use Exception;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Tests\Event\Stub\EventStub;
use Tests\Event\Stub\EventStubA;
use Tests\Event\Stub\EventStubB;
use Tests\Projection\Stub\ProjectionStub;
use Tests\Resources\Views\Stub\WebfProjectionsDataCollectorStub;
use Tests\Service\Stub\FakeTraceableEventProjector;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Webf\Projections\Bundle\DataCollector\WebfProjectionsDataCollector;
use Webf\Projections\Bundle\DependencyInjection\WebfProjectionsExtension;
use Webf\Projections\Bundle\Test\KernelTestCase;
use Webf\Projections\Core\Event\InMemoryEventClassManager;
use Webf\Projections\Core\Projection\ProjectionFactory;
use Webf\Projections\Core\Service\Data\Caller;
use Webf\Projections\Core\Service\Data\DispatchedEvent;
use Webf\Projections\Core\Service\ProjectionManager;
/**
* @internal
......@@ -27,7 +32,17 @@ class DataCollectorTest extends KernelTestCase
public function test_no_event()
{
[$toolbar, $menu, $panel] = $this->getCrawlers(
new WebfProjectionsDataCollectorStub()
$collector = new WebfProjectionsDataCollector(
new InMemoryEventClassManager($eventClasses = [
EventStub::class,
]),
new FakeTraceableEventProjector($events = []),
new ProjectionManager([
new ProjectionFactory($allProjections = [
new ProjectionStub(),
]),
])
)
);
$this->assertNoToolbar($toolbar);
......@@ -40,10 +55,13 @@ class DataCollectorTest extends KernelTestCase
$this->assertPanelData($panel, [
'projection_metrics' => [
'nb-events' => '0',
'nb-event-types' => '0/0',
'nb-projections' => '0/0',
'execution-time' => '0.00 ms',
'nb-events' => count($events),
'nb-event-types' => '0/' . count($eventClasses),
'nb-projections' => '0/' . count($allProjections),
'execution-time' => sprintf(
'%.2F ms',
$collector->getExecutionTime()
),
],
'dispatched_events' => false,
]);
......@@ -55,50 +73,65 @@ class DataCollectorTest extends KernelTestCase
$event = new EventStub(),
$caller = new Caller('/path/to/file.php', 42)
);
$dispatchedEvent->projectionStarted($projection = new ProjectionStub());
$dispatchedEvent->projectionStarted($projections[] = new ProjectionStub());
usleep(1);
$dispatchedEvent->projectionSucceeded();
$dispatchedEvent->dispatched();
[$toolbar, $menu, $panel] = $this->getCrawlers(
new WebfProjectionsDataCollectorStub([
'nb_events' => 1,
'nb_successful_events' => 1,
'nb_failed_events' => 0,
'nb_orphaned_events' => 0,
'nb_event_types' => 1,
'nb_total_event_types' => 3,
'nb_projections' => 1,
'nb_total_projections' => 2,
'execution_time' => 123.456,
'events' => [$dispatchedEvent],
])
$collector = new WebfProjectionsDataCollector(
new InMemoryEventClassManager($eventClasses = [
EventStub::class,
EventStubA::class,
EventStubB::class,
]),
new FakeTraceableEventProjector($events = [$dispatchedEvent]),
new ProjectionManager([
new ProjectionFactory($allProjections = [
new ProjectionStub(),
new ProjectionStub(),
]),
])
)
);
$this->assertToolbarData($toolbar, [
'status' => 'normal',
'icon' => '1 in 123.46 ms',
'icon' => sprintf(
'%s in %.2F ms',
count($events),
$collector->getExecutionTime()
),
'info_pieces' => [
'successful' => ['value' => '1', 'status' => 'green'],
'failed' => ['value' => '0', 'status' => 'normal'],
'orphaned' => ['value' => '0', 'status' => 'normal'],
'total' => ['value' => '1', 'status' => 'normal'],
'execution-time' => ['value' => '123.46 ms', 'status' => false],
'execution-time' => [
'value' => sprintf(
'%.2F ms',
$collector->getExecutionTime()
),
'status' => false,
],
],
]);
$this->assertMenuData($menu, [
'count' => '1',
'count' => count($events),
'disabled' => false,
'status' => 'normal',
]);
$this->assertPanelData($panel, [
'projection_metrics' => [
'nb-events' => '1',
'nb-event-types' => '1/3',
'nb-projections' => '1/2',
'execution-time' => '123.46 ms',
'nb-events' => count($events),
'nb-event-types' => '1/' . count($eventClasses),
'nb-projections' => count($projections) . '/' . count($allProjections),
'execution-time' => sprintf(
'%.2F ms',
$collector->getExecutionTime()
),
],
'dispatched_events' => [
'all' => [$dispatchedEvent],
......@@ -115,50 +148,65 @@ class DataCollectorTest extends KernelTestCase
$event = new EventStub(),
$caller = new Caller('/path/to/file.php', 42)
);
$dispatchedEvent->projectionStarted($projection = new ProjectionStub());
$dispatchedEvent->projectionStarted($projections[] = new ProjectionStub());
usleep(1);
$dispatchedEvent->projectionFailed(new Exception());
$dispatchedEvent->dispatched();
[$toolbar, $menu, $panel] = $this->getCrawlers(
new WebfProjectionsDataCollectorStub([
'nb_events' => 1,
'nb_successful_events' => 0,
'nb_failed_events' => 1,
'nb_orphaned_events' => 0,
'nb_event_types' => 1,
'nb_total_event_types' => 3,
'nb_projections' => 1,
'nb_total_projections' => 2,
'execution_time' => 123.456,
'events' => [$dispatchedEvent],
])
$collector = new WebfProjectionsDataCollector(
new InMemoryEventClassManager($eventClasses = [
EventStub::class,
EventStubA::class,
EventStubB::class,
]),
new FakeTraceableEventProjector($events = [$dispatchedEvent]),
new ProjectionManager([
new ProjectionFactory($allProjections = [
new ProjectionStub(),
new ProjectionStub(),
]),
])
)
);
$this->assertToolbarData($toolbar, [
'status' => 'red',
'icon' => '1 in 123.46 ms',
'icon' => sprintf(
'%s in %.2F ms',
count($events),
$collector->getExecutionTime()
),
'info_pieces' => [
'successful' => ['value' => '0', 'status' => 'normal'],
'failed' => ['value' => '1', 'status' => 'red'],
'orphaned' => ['value' => '0', 'status' => 'normal'],
'total' => ['value' => '1', 'status' => 'normal'],
'execution-time' => ['value' => '123.46 ms', 'status' => false],
'execution-time' => [
'value' => sprintf(
'%.2F ms',
$collector->getExecutionTime()
),