Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for debugging tests #360

Merged
merged 9 commits into from
Dec 24, 2019
18 changes: 18 additions & 0 deletions Clockwork/Clockwork.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ public function resolveAsQueueJob($name, $description = null, $status = 'process
return $this;
}

// Resolve the current request as a "test" type request with test-specific data, accepts test name, status, status
// message in case of failure and array of ran asserts
public function resolveAsTest($name, $status = 'passed', $statusMessage = null, $asserts = [])
{
$this->resolveRequest();

$this->request->type = RequestType::TEST;
$this->request->testName = $name;
$this->request->testStatus = $status;
$this->request->testStatusMessage = $statusMessage;

foreach ($asserts as $assert) {
$this->request->addTestAssert($assert['name'], $assert['arguments'], $assert['passed'], $assert['trace']);
}

return $this;
}

// Extends the request with additional data form all data sources when being shown in the Clockwork app
public function extendRequest(Request $request = null)
{
Expand Down
16 changes: 14 additions & 2 deletions Clockwork/Helpers/StackTrace.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ public static function get($options = [])
{
$backtraceOptions = isset($options['arguments'])
? DEBUG_BACKTRACE_PROVIDE_OBJECT : DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS;
$limit = isset($options['limit']) ? $options['limit'] : 0;

return static::from(debug_backtrace($backtraceOptions));
return static::from(debug_backtrace($backtraceOptions, $limit));
}

public static function from(array $trace)
Expand Down Expand Up @@ -52,11 +53,22 @@ public function first($filter = null)
}
}

public function last($filter = null)
{
if (! $filter) return $this->frames[count($this->frames) - 1];

if ($filter instanceof StackFilter) $filter = $filter->closure();

foreach (array_reverse($this->frames) as $frame) {
if ($filter($frame)) return $frame;
}
}

public function filter($filter = null)
{
if ($filter instanceof StackFilter) $filter = $filter->closure();

return $this->copy(array_filter($this->frames, $filter));
return $this->copy(array_values(array_filter($this->frames, $filter)));
}

public function skip($count = null)
Expand Down
27 changes: 27 additions & 0 deletions Clockwork/Request/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ class Request
// Queue job additional options
public $jobOptions = [];

// Test name
public $testName;

// Test status
public $testStatus;

// Test status message (eg. in case of failure)
public $testStatusMessage;

// Ran test asserts
public $testAsserts = [];

// Parent request
public $parent;

Expand Down Expand Up @@ -333,6 +345,10 @@ public function toArray()
'jobQueue' => $this->jobQueue,
'jobConnection' => $this->jobConnection,
'jobOptions' => $this->jobOptions,
'testName' => $this->testName,
'testStatus' => $this->testStatus,
'testStatusMessage' => $this->testStatusMessage,
'testAsserts' => $this->testAsserts,
'parent' => $this->parent
];
}
Expand Down Expand Up @@ -478,6 +494,17 @@ public function userData($key = null)
return $key ? $this->userData[$key] = $userData : $this->userData[] = $userData;
}

// Add a ran test assert, takes the assert name, arguments, whether it passed and trace as arguments
public function addTestAssert($name, $arguments = null, $passed = true, $trace = null)
{
$this->testAsserts[] = [
'name' => $name,
'arguments' => (new Serializer)->normalize($arguments),
'trace' => $trace,
'passed' => $passed
];
}

/**
* Generate unique request ID in form <current time>-<random number>
*/
Expand Down
1 change: 1 addition & 0 deletions Clockwork/Request/RequestType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ class RequestType
const REQUEST = 'request';
const COMMAND = 'command';
const QUEUE_JOB = 'queue-job';
const TEST = 'test';
}
4 changes: 4 additions & 0 deletions Clockwork/Storage/FileStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ protected function makeRequestFromIndex($record)
$nameField = 'commandName';
} elseif ($type == 'queue-job') {
$nameField = 'jobName';
} elseif ($type == 'test') {
$nameField = 'testName';
} else {
$nameField = 'uri';
}
Expand All @@ -288,6 +290,8 @@ protected function updateIndex(Request $request)
$nameField = 'commandName';
} elseif ($request->type == 'queue-job') {
$nameField = 'jobName';
} elseif ($request->type == 'test') {
$nameField = 'testName';
} else {
$nameField = 'uri';
}
Expand Down
11 changes: 11 additions & 0 deletions Clockwork/Storage/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function matches(Request $request)
return $this->matchesCommand($request);
} elseif ($request->type == RequestType::QUEUE_JOB) {
return $this->matchesQueueJob($request);
} elseif ($request->type == RequestType::TEST) {
return $this->matchesTest($request);
} else {
return $this->matchesRequest($request);
}
Expand Down Expand Up @@ -68,6 +70,15 @@ protected function matchesQueueJob(Request $request)
&& $this->matchesDate($this->received, $request->time);
}

protected function matchesTest(Request $request)
{
return $this->matchesString($this->type, RequestType::TEST)
&& $this->matchesString($this->name, $request->testName)
&& $this->matchesString($this->status, $request->testStatus)
&& $this->matchesNumber($this->time, $request->responseDuration)
&& $this->matchesDate($this->received, $request->time);
}

public function isEmpty()
{
return ! count($this->uri) && ! count($this->controller) && ! count($this->method) && ! count($this->status)
Expand Down
4 changes: 2 additions & 2 deletions Clockwork/Storage/SqlSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ protected function resolveConditions()

$conditions = array_filter([
$this->resolveStringCondition([ 'type' ], $this->type),
$this->resolveStringCondition([ 'uri', 'commandName', 'jobName' ], array_merge($this->uri, $this->name)),
$this->resolveStringCondition([ 'uri', 'commandName', 'jobName', 'testName' ], array_merge($this->uri, $this->name)),
$this->resolveStringCondition([ 'controller' ], $this->controller),
$this->resolveExactCondition([ 'method' ], $this->method),
$this->resolveNumberCondition([ 'responseStatus', 'commandExitCode', 'jobStatus' ], $this->status),
$this->resolveNumberCondition([ 'responseStatus', 'commandExitCode', 'jobStatus', 'testStatus' ], $this->status),
$this->resolveNumberCondition([ 'responseDuration' ], $this->time),
$this->resolveDateCondition([ 'time' ], $this->received)
]);
Expand Down
17 changes: 16 additions & 1 deletion Clockwork/Support/Laravel/ClockworkServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Clockwork\DataSource\XdebugDataSource;
use Clockwork\Helpers\StackFilter;
use Clockwork\Request\Log;
use Clockwork\Request\Request;
use Clockwork\Storage\StorageInterface;

use Illuminate\Redis\RedisManager;
Expand Down Expand Up @@ -74,6 +75,7 @@ public function register()
$clockwork = (new Clockwork)
->setAuthenticator($app['clockwork.authenticator'])
->setLog($app['clockwork.log'])
->setRequest($app['clockwork.request'])
->setStorage($app['clockwork.storage'])
->addDataSource(new PhpDataSource())
->addDataSource($app['clockwork.laravel']);
Expand All @@ -97,6 +99,10 @@ public function register()
return new Log;
});

$this->app->singleton('clockwork.request', function ($app) {
return new Request;
});

$this->app->singleton('clockwork.storage', function ($app) {
return $app['clockwork.support']->getStorage();
});
Expand All @@ -109,8 +115,8 @@ public function register()
$this->registerDataSources();
$this->registerAliases();

$this->app->make('clockwork.request'); // instantiate the request to have id and time available as early as possible
$this->app['clockwork.support']->configureSerializer();

$this->app['clockwork.laravel']->listenToEarlyEvents();

if ($this->app['clockwork.support']->getConfig('register_helpers', true)) {
Expand Down Expand Up @@ -153,6 +159,15 @@ protected function registerDataSources()
}, 'early');
}

if ($app->runningUnitTests()) {
$dataSource->addFilter(function ($query, $trace) {
return ! $trace->first(StackFilter::make()->isClass([
\Illuminate\Database\Migrations\Migrator::class,
\Illuminate\Database\Console\Migrations\MigrateCommand::class
]));
});
}

return $dataSource;
});

Expand Down
17 changes: 16 additions & 1 deletion Clockwork/Support/Laravel/ClockworkSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ public function isCollectingData()
{
return $this->isCollectingCommands()
|| $this->isCollectingQueueJobs()
|| $this->isCollectingRequests();
|| $this->isCollectingRequests()
|| $this->isCollectingTests();
}

public function isCollectingCommands()
Expand All @@ -304,6 +305,13 @@ public function isCollectingRequests()
&& ! $this->isUriFiltered($this->app['request']->getRequestUri());
}

public function isCollectingTests()
{
return ($this->isEnabled() || $this->getConfig('collect_data_always', false))
&& $this->app->runningInConsole()
&& $this->getConfig('tests.collect', false);
}

public function isFeatureEnabled($feature)
{
return $this->getConfig("features.{$feature}.enabled") && $this->isFeatureAvailable($feature);
Expand Down Expand Up @@ -376,6 +384,13 @@ protected function isQueueJobFiltered($queueJob)
return in_array($queueJob, $blacklist);
}

public function isTestFiltered($test)
{
$blacklist = $this->getConfig('tests.except', []);

return in_array($test, $blacklist);
}

protected function appendServerTimingHeader($response, $request)
{
if (($eventsCount = $this->getConfig('server_timing', 10)) !== false) {
Expand Down
79 changes: 79 additions & 0 deletions Clockwork/Support/Laravel/Tests/UsesClockwork.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php namespace Clockwork\Support\Laravel\Tests;

use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackFilter;
use Clockwork\Helpers\StackTrace;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Runner\BaseTestRunner;

trait UsesClockwork
{
protected static $clockwork = [
'asserts' => []
];

protected function setUpClockwork()
{
$this->beforeApplicationDestroyed(function () {
if ($this->app->make('clockwork.support')->isTestFiltered($this->toString())) return;

$this->app->make('clockwork')
->resolveAsTest(
$this->toString(),
$this->resolveClockworkStatus(),
$this->getStatusMessage(),
$this->resolveClockworkAsserts()
)
->storeRequest();
});
}

protected function resolveClockworkStatus()
{
$status = $this->getStatus();

$statuses = [
BaseTestRunner::STATUS_UNKNOWN => 'unknown',
BaseTestRunner::STATUS_PASSED => 'passed',
BaseTestRunner::STATUS_SKIPPED => 'skipped',
BaseTestRunner::STATUS_INCOMPLETE => 'incomplete',
BaseTestRunner::STATUS_FAILURE => 'failed',
BaseTestRunner::STATUS_ERROR => 'error',
BaseTestRunner::STATUS_RISKY => 'passed',
BaseTestRunner::STATUS_WARNING => 'warning'
];

return isset($statuses[$status]) ? $statuses[$status] : null;
}

protected function resolveClockworkAsserts()
{
$asserts = static::$clockwork['asserts'];

if ($this->getStatus() == BaseTestRunner::STATUS_FAILURE) {
$asserts[count($asserts) - 1]['passed'] = false;
}

static::$clockwork['asserts'] = [];

return $asserts;
}

public static function assertThat($value, Constraint $constraint, string $message = ''): void
{
$trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]);

$assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last();
$trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3);

static::$clockwork['asserts'][] = [
'name' => $assertFrame->function,
'arguments' => $assertFrame->args,
'trace' => (new Serializer)->trace($trace),
'passed' => true
];

parent::assertThat($value, $constraint, $message);
}
}
19 changes: 19 additions & 0 deletions Clockwork/Support/Laravel/config/clockwork.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,25 @@
]
],

/*
|--------------------------------------------------------------------------
| Tests collection
|--------------------------------------------------------------------------
|
| You can enable or disable and configure collection of ran tests here.
|
*/

'tests' => [
// Enable or disable collection of ran tests
'collect' => env('CLOCKWORK_TESTS_COLLECT', false),

// List of tests that should not be collected
'except' => [
// Tests\Unit\ExampleTest::class
]
],

/*
|--------------------------------------------------------------------------
| Enable data collection, when Clockwork is disabled
Expand Down