This commit is contained in:
2020-03-27 10:13:51 +07:00
commit da1024a5b3
16614 changed files with 3274282 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<?php
namespace Codeception\Lib\Actor\Shared;
trait Comment
{
/**
* @return \Codeception\Scenario
*/
abstract protected function getScenario();
public function expectTo($prediction)
{
return $this->comment('I expect to ' . $prediction);
}
public function expect($prediction)
{
return $this->comment('I expect ' . $prediction);
}
public function amGoingTo($argumentation)
{
return $this->comment('I am going to ' . $argumentation);
}
public function am($role)
{
$role = trim($role);
if (stripos('aeiou', $role[0]) !== false) {
return $this->comment('As an ' . $role);
}
return $this->comment('As a ' . $role);
}
public function lookForwardTo($achieveValue)
{
return $this->comment('So that I ' . $achieveValue);
}
public function comment($description)
{
$this->getScenario()->comment($description);
return $this;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Codeception\Lib\Actor\Shared;
use Codeception\Lib\Friend as LibFriend;
use Codeception\Scenario;
trait Friend
{
protected $friends = [];
/**
* @return Scenario
*/
abstract protected function getScenario();
/**
* @param $name
* @param $actorClass
* @return \Codeception\Lib\Friend
*/
public function haveFriend($name, $actorClass = null)
{
if (!isset($this->friends[$name])) {
$actor = $actorClass === null ? $this : new $actorClass($this->getScenario());
$this->friends[$name] = new LibFriend($name, $actor, $this->getScenario()->current('modules'));
}
return $this->friends[$name];
}
}

View File

@@ -0,0 +1,295 @@
<?php
namespace Codeception\Lib\Connector;
use Aws\Credentials\Credentials;
use Aws\Signature\SignatureV4;
use Codeception\Util\Uri;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Post\PostFile;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response as BrowserKitResponse;
use GuzzleHttp\Url;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
class Guzzle extends Client
{
protected $baseUri;
protected $requestOptions = [
'allow_redirects' => false,
'headers' => [],
];
protected $refreshMaxInterval = 0;
protected $awsCredentials = null;
protected $awsSignature = null;
/** @var \GuzzleHttp\Client */
protected $client;
public function setBaseUri($uri)
{
$this->baseUri = $uri;
}
/**
* Sets the maximum allowable timeout interval for a meta tag refresh to
* automatically redirect a request.
*
* A meta tag detected with an interval equal to or greater than $seconds
* would not result in a redirect. A meta tag without a specified interval
* or one with a value less than $seconds would result in the client
* automatically redirecting to the specified URL
*
* @param int $seconds Number of seconds
*/
public function setRefreshMaxInterval($seconds)
{
$this->refreshMaxInterval = $seconds;
}
public function setClient(\GuzzleHttp\Client $client)
{
$this->client = $client;
}
/**
* Sets the request header to the passed value. The header will be
* sent along with the next request.
*
* Passing an empty value clears the header, which is the equivalent
* of calling deleteHeader.
*
* @param string $name the name of the header
* @param string $value the value of the header
*/
public function setHeader($name, $value)
{
if (strval($value) === '') {
$this->deleteHeader($name);
} else {
$this->requestOptions['headers'][$name] = $value;
}
}
/**
* Deletes the header with the passed name from the list of headers
* that will be sent with the request.
*
* @param string $name the name of the header to delete.
*/
public function deleteHeader($name)
{
unset($this->requestOptions['headers'][$name]);
}
/**
* @param string $username
* @param string $password
* @param string $type Default: 'basic'
*/
public function setAuth($username, $password, $type = 'basic')
{
if (!$username) {
unset($this->requestOptions['auth']);
return;
}
$this->requestOptions['auth'] = [$username, $password, $type];
}
/**
* Taken from Mink\BrowserKitDriver
*
* @param Response $response
*
* @return \Symfony\Component\BrowserKit\Response
*/
protected function createResponse(Response $response)
{
$contentType = $response->getHeader('Content-Type');
if (!$contentType) {
$contentType = 'text/html';
}
if (strpos($contentType, 'charset=') === false) {
$body = $response->getBody(true);
if (preg_match('/\<meta[^\>]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) {
$contentType .= ';charset=' . $matches[1];
}
$response->setHeader('Content-Type', $contentType);
}
$headers = $response->getHeaders();
$status = $response->getStatusCode();
if ($status < 300 || $status >= 400) {
$matches = [];
$matchesMeta = preg_match(
'/\<meta[^\>]+http-equiv="refresh" content="\s*(\d*)\s*;\s*url=(.*?)"/i',
$response->getBody(true),
$matches
);
if (!$matchesMeta) {
// match by header
preg_match(
'/^\s*(\d*)\s*;\s*url=(.*)/i',
(string)$response->getHeader('Refresh'),
$matches
);
}
if ((!empty($matches)) && (empty($matches[1]) || $matches[1] < $this->refreshMaxInterval)) {
$uri = $this->getAbsoluteUri($matches[2]);
$partsUri = parse_url($uri);
$partsCur = parse_url($this->getHistory()->current()->getUri());
foreach ($partsCur as $key => $part) {
if ($key === 'fragment') {
continue;
}
if (!isset($partsUri[$key]) || $partsUri[$key] !== $part) {
$status = 302;
$headers['Location'] = $matchesMeta ? htmlspecialchars_decode($uri) : $uri;
break;
}
}
}
}
return new BrowserKitResponse($response->getBody(), $status, $headers);
}
public function getAbsoluteUri($uri)
{
$baseUri = $this->baseUri;
if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) {
if (strpos($uri, '/') === 0) {
$baseUriPath = parse_url($baseUri, PHP_URL_PATH);
if (!empty($baseUriPath) && strpos($uri, $baseUriPath) === 0) {
$uri = substr($uri, strlen($baseUriPath));
}
return Uri::appendPath((string)$baseUri, $uri);
}
// relative url
if (!$this->getHistory()->isEmpty()) {
return Uri::mergeUrls((string)$this->getHistory()->current()->getUri(), $uri);
}
}
return Uri::mergeUrls($baseUri, $uri);
}
protected function doRequest($request)
{
/** @var $request BrowserKitRequest **/
$requestOptions = [
'body' => $this->extractBody($request),
'cookies' => $this->extractCookies($request),
'headers' => $this->extractHeaders($request)
];
$requestOptions = array_replace_recursive($requestOptions, $this->requestOptions);
$guzzleRequest = $this->client->createRequest(
$request->getMethod(),
$request->getUri(),
$requestOptions
);
foreach ($this->extractFiles($request) as $postFile) {
$guzzleRequest->getBody()->addFile($postFile);
}
// Let BrowserKit handle redirects
try {
if (null !== $this->awsCredentials) {
$response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials));
} else {
$response = $this->client->send($guzzleRequest);
}
} catch (RequestException $e) {
if ($e->hasResponse()) {
$response = $e->getResponse();
} else {
throw $e;
}
}
return $this->createResponse($response);
}
protected function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
protected function extractBody(BrowserKitRequest $request)
{
if (in_array(strtoupper($request->getMethod()), ['GET', 'HEAD'])) {
return null;
}
if ($request->getContent() !== null) {
return $request->getContent();
}
return $request->getParameters();
}
protected function extractFiles(BrowserKitRequest $request)
{
if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT'])) {
return [];
}
return $this->mapFiles($request->getFiles());
}
protected function mapFiles($requestFiles, $arrayName = '')
{
$files = [];
foreach ($requestFiles as $name => $info) {
if (!empty($arrayName)) {
$name = $arrayName.'['.$name.']';
}
if (is_array($info)) {
if (isset($info['tmp_name'])) {
if ($info['tmp_name']) {
$handle = fopen($info['tmp_name'], 'r');
$filename = isset($info['name']) ? $info['name'] : null;
$files[] = new PostFile($name, $handle, $filename);
}
} else {
$files = array_merge($files, $this->mapFiles($info, $name));
}
} else {
$files[] = new PostFile($name, fopen($info, 'r'));
}
}
return $files;
}
protected function extractCookies(BrowserKitRequest $request)
{
return $this->getCookieJar()->allRawValues($request->getUri());
}
public function setAwsAuth($config)
{
$this->awsCredentials = new Credentials($config['key'], $config['secret']);
$this->awsSignature = new SignatureV4($config['service'], $config['region']);
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace Codeception\Lib\Connector;
use Aws\Credentials\Credentials;
use Aws\Signature\SignatureV4;
use Codeception\Util\Uri;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\SetCookie;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\StreamHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request as Psr7Request;
use GuzzleHttp\Psr7\Response as Psr7Response;
use GuzzleHttp\Psr7\Uri as Psr7Uri;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response as BrowserKitResponse;
class Guzzle6 extends Client
{
protected $requestOptions = [
'allow_redirects' => false,
'headers' => [],
];
protected $refreshMaxInterval = 0;
protected $awsCredentials = null;
protected $awsSignature = null;
/** @var \GuzzleHttp\Client */
protected $client;
/**
* Sets the maximum allowable timeout interval for a meta tag refresh to
* automatically redirect a request.
*
* A meta tag detected with an interval equal to or greater than $seconds
* would not result in a redirect. A meta tag without a specified interval
* or one with a value less than $seconds would result in the client
* automatically redirecting to the specified URL
*
* @param int $seconds Number of seconds
*/
public function setRefreshMaxInterval($seconds)
{
$this->refreshMaxInterval = $seconds;
}
public function setClient(GuzzleClient &$client)
{
$this->client = $client;
}
/**
* Sets the request header to the passed value. The header will be
* sent along with the next request.
*
* Passing an empty value clears the header, which is the equivalent
* of calling deleteHeader.
*
* @param string $name the name of the header
* @param string $value the value of the header
*/
public function setHeader($name, $value)
{
if (strval($value) === '') {
$this->deleteHeader($name);
} else {
$this->requestOptions['headers'][$name] = $value;
}
}
/**
* Deletes the header with the passed name from the list of headers
* that will be sent with the request.
*
* @param string $name the name of the header to delete.
*/
public function deleteHeader($name)
{
unset($this->requestOptions['headers'][$name]);
}
/**
* @param string $username
* @param string $password
* @param string $type Default: 'basic'
*/
public function setAuth($username, $password, $type = 'basic')
{
if (!$username) {
unset($this->requestOptions['auth']);
return;
}
$this->requestOptions['auth'] = [$username, $password, $type];
}
/**
* Taken from Mink\BrowserKitDriver
*
* @param Response $response
*
* @return \Symfony\Component\BrowserKit\Response
*/
protected function createResponse(Psr7Response $response)
{
$body = (string) $response->getBody();
$headers = $response->getHeaders();
$contentType = null;
if (isset($headers['Content-Type'])) {
$contentType = reset($headers['Content-Type']);
}
if (!$contentType) {
$contentType = 'text/html';
}
if (strpos($contentType, 'charset=') === false) {
if (preg_match('/\<meta[^\>]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) {
$contentType .= ';charset=' . $matches[1];
}
$headers['Content-Type'] = [$contentType];
}
$status = $response->getStatusCode();
if ($status < 300 || $status >= 400) {
$matches = [];
$matchesMeta = preg_match(
'/\<meta[^\>]+http-equiv="refresh" content="\s*(\d*)\s*;\s*url=(.*?)"/i',
$body,
$matches
);
if (!$matchesMeta && isset($headers['Refresh'])) {
// match by header
preg_match(
'/^\s*(\d*)\s*;\s*url=(.*)/i',
(string) reset($headers['Refresh']),
$matches
);
}
if ((!empty($matches)) && (empty($matches[1]) || $matches[1] < $this->refreshMaxInterval)) {
$uri = new Psr7Uri($this->getAbsoluteUri($matches[2]));
$currentUri = new Psr7Uri($this->getHistory()->current()->getUri());
if ($uri->withFragment('') != $currentUri->withFragment('')) {
$status = 302;
$headers['Location'] = $matchesMeta ? htmlspecialchars_decode($uri) : (string)$uri;
}
}
}
return new BrowserKitResponse($body, $status, $headers);
}
public function getAbsoluteUri($uri)
{
$baseUri = $this->client->getConfig('base_uri');
if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) {
if (strpos($uri, '/') === 0) {
$baseUriPath = $baseUri->getPath();
if (!empty($baseUriPath) && strpos($uri, $baseUriPath) === 0) {
$uri = substr($uri, strlen($baseUriPath));
}
return Uri::appendPath((string)$baseUri, $uri);
}
// relative url
if (!$this->getHistory()->isEmpty()) {
return Uri::mergeUrls((string)$this->getHistory()->current()->getUri(), $uri);
}
}
return Uri::mergeUrls($baseUri, $uri);
}
protected function doRequest($request)
{
/** @var $request BrowserKitRequest **/
$guzzleRequest = new Psr7Request(
$request->getMethod(),
$request->getUri(),
$this->extractHeaders($request),
$request->getContent()
);
$options = $this->requestOptions;
$options['cookies'] = $this->extractCookies($guzzleRequest->getUri()->getHost());
$multipartData = $this->extractMultipartFormData($request);
if (!empty($multipartData)) {
$options['multipart'] = $multipartData;
}
$formData = $this->extractFormData($request);
if (empty($multipartData) and $formData) {
$options['form_params'] = $formData;
}
try {
if (null !== $this->awsCredentials) {
$response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials), $options);
} else {
$response = $this->client->send($guzzleRequest, $options);
}
} catch (RequestException $e) {
if (!$e->hasResponse()) {
throw $e;
}
$response = $e->getResponse();
}
return $this->createResponse($response);
}
protected function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
protected function extractFormData(BrowserKitRequest $request)
{
if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
return null;
}
// guessing if it is a form data
$headers = $request->getServer();
if (isset($headers['HTTP_CONTENT_TYPE'])) {
// not a form
if ($headers['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') {
return null;
}
}
if ($request->getContent() !== null) {
return null;
}
return $request->getParameters();
}
protected function extractMultipartFormData(Request $request)
{
if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH'])) {
return [];
}
$parts = $this->mapFiles($request->getFiles());
if (empty($parts)) {
return [];
}
foreach ($request->getParameters() as $k => $v) {
$parts = $this->formatMultipart($parts, $k, $v);
}
return $parts;
}
protected function formatMultipart($parts, $key, $value)
{
if (is_array($value)) {
foreach ($value as $subKey => $subValue) {
$parts = array_merge($this->formatMultipart([], $key."[$subKey]", $subValue), $parts);
}
return $parts;
}
$parts[] = ['name' => $key, 'contents' => (string) $value];
return $parts;
}
protected function mapFiles($requestFiles, $arrayName = '')
{
$files = [];
foreach ($requestFiles as $name => $info) {
if (!empty($arrayName)) {
$name = $arrayName . '[' . $name . ']';
}
if (is_array($info)) {
if (isset($info['tmp_name'])) {
if ($info['tmp_name']) {
$handle = fopen($info['tmp_name'], 'r');
$filename = isset($info['name']) ? $info['name'] : null;
$files[] = [
'name' => $name,
'contents' => $handle,
'filename' => $filename
];
}
} else {
$files = array_merge($files, $this->mapFiles($info, $name));
}
} else {
$files[] = [
'name' => $name,
'contents' => fopen($info, 'r')
];
}
}
return $files;
}
protected function extractCookies($host)
{
$jar = [];
$cookies = $this->getCookieJar()->all();
foreach ($cookies as $cookie) {
/** @var $cookie Cookie **/
$setCookie = SetCookie::fromString((string)$cookie);
if (!$setCookie->getDomain()) {
$setCookie->setDomain($host);
}
$jar[] = $setCookie;
}
return new CookieJar(false, $jar);
}
public static function createHandler($handler)
{
if ($handler === 'curl') {
return HandlerStack::create(new CurlHandler());
}
if ($handler === 'stream') {
return HandlerStack::create(new StreamHandler());
}
if (class_exists($handler)) {
return HandlerStack::create(new $handler);
}
if (is_callable($handler)) {
return HandlerStack::create($handler);
}
return HandlerStack::create();
}
public function setAwsAuth($config)
{
$this->awsCredentials = new Credentials($config['key'], $config['secret']);
$this->awsSignature = new SignatureV4($config['service'], $config['region']);
}
}

View File

@@ -0,0 +1,354 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\Laravel5\ExceptionHandlerDecorator;
use Codeception\Lib\Connector\Shared\LaravelCommon;
use Codeception\Stub;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Client;
class Laravel5 extends Client
{
use LaravelCommon;
/**
* @var Application
*/
private $app;
/**
* @var \Codeception\Module\Laravel5
*/
private $module;
/**
* @var bool
*/
private $firstRequest = true;
/**
* @var array
*/
private $triggeredEvents = [];
/**
* @var bool
*/
private $exceptionHandlingDisabled;
/**
* @var bool
*/
private $middlewareDisabled;
/**
* @var bool
*/
private $eventsDisabled;
/**
* @var bool
*/
private $modelEventsDisabled;
/**
* @var object
*/
private $oldDb;
/**
* Constructor.
*
* @param \Codeception\Module\Laravel5 $module
*/
public function __construct($module)
{
$this->module = $module;
$this->exceptionHandlingDisabled = $this->module->config['disable_exception_handling'];
$this->middlewareDisabled = $this->module->config['disable_middleware'];
$this->eventsDisabled = $this->module->config['disable_events'];
$this->modelEventsDisabled = $this->module->config['disable_model_events'];
$this->initialize();
$components = parse_url($this->app['config']->get('app.url', 'http://localhost'));
if (array_key_exists('url', $this->module->config)) {
$components = parse_url($this->module->config['url']);
}
$host = isset($components['host']) ? $components['host'] : 'localhost';
parent::__construct($this->app, ['HTTP_HOST' => $host]);
// Parent constructor defaults to not following redirects
$this->followRedirects(true);
}
/**
* Execute a request.
*
* @param SymfonyRequest $request
* @return Response
*/
protected function doRequest($request)
{
if (!$this->firstRequest) {
$this->initialize($request);
}
$this->firstRequest = false;
$this->applyBindings();
$this->applyContextualBindings();
$this->applyInstances();
$this->applyApplicationHandlers();
$request = Request::createFromBase($request);
$response = $this->kernel->handle($request);
$this->app->make('Illuminate\Contracts\Http\Kernel')->terminate($request, $response);
return $response;
}
/**
* Make sure files are \Illuminate\Http\UploadedFile instances with the private $test property set to true.
* Fixes issue https://github.com/Codeception/Codeception/pull/3417.
*
* @param array $files
* @return array
*/
protected function filterFiles(array $files)
{
$files = parent::filterFiles($files);
if (! class_exists('Illuminate\Http\UploadedFile')) {
// The \Illuminate\Http\UploadedFile class was introduced in Laravel 5.2.15,
// so don't change the $files array if it does not exist.
return $files;
}
return $this->convertToTestFiles($files);
}
/**
* @param array $files
* @return array
*/
private function convertToTestFiles(array $files)
{
$filtered = [];
foreach ($files as $key => $value) {
if (is_array($value)) {
$filtered[$key] = $this->convertToTestFiles($value);
} else {
$filtered[$key] = UploadedFile::createFromBase($value, true);
}
}
return $filtered;
}
/**
* Initialize the Laravel framework.
*
* @param SymfonyRequest $request
*/
private function initialize($request = null)
{
// Store a reference to the database object
// so the database connection can be reused during tests
$this->oldDb = null;
if (isset($this->app['db']) && $this->app['db']->connection()) {
$this->oldDb = $this->app['db'];
}
$this->app = $this->kernel = $this->loadApplication();
// Set the request instance for the application,
if (is_null($request)) {
$appConfig = require $this->module->config['project_dir'] . 'config/app.php';
$request = SymfonyRequest::create($appConfig['url']);
}
$this->app->instance('request', Request::createFromBase($request));
// Reset the old database after all the service providers are registered.
if ($this->oldDb) {
$this->app['events']->listen('bootstrapped: Illuminate\Foundation\Bootstrap\RegisterProviders', function () {
$this->app->singleton('db', function () {
return $this->oldDb;
});
});
}
$this->app->make('Illuminate\Contracts\Http\Kernel')->bootstrap();
// Record all triggered events by adding a wildcard event listener
// Since Laravel 5.4 wildcard event handlers receive the event name as the first argument,
// but for earlier Laravel versions the firing() method of the event dispatcher should be used
// to determine the event name.
if (method_exists($this->app['events'], 'firing')) {
$listener = function () {
$this->triggeredEvents[] = $this->normalizeEvent($this->app['events']->firing());
};
} else {
$listener = function ($event) {
$this->triggeredEvents[] = $this->normalizeEvent($event);
};
}
$this->app['events']->listen('*', $listener);
// Replace the Laravel exception handler with our decorated exception handler,
// so exceptions can be intercepted for the disable_exception_handling functionality.
$decorator = new ExceptionHandlerDecorator($this->app['Illuminate\Contracts\Debug\ExceptionHandler']);
$decorator->exceptionHandlingDisabled($this->exceptionHandlingDisabled);
$this->app->instance('Illuminate\Contracts\Debug\ExceptionHandler', $decorator);
if ($this->module->config['disable_middleware'] || $this->middlewareDisabled) {
$this->app->instance('middleware.disable', true);
}
if ($this->module->config['disable_events'] || $this->eventsDisabled) {
$this->mockEventDispatcher();
}
if ($this->module->config['disable_model_events'] || $this->modelEventsDisabled) {
Model::unsetEventDispatcher();
}
$this->module->setApplication($this->app);
}
/**
* Boot the Laravel application object.
* @return Application
* @throws ModuleConfig
*/
private function loadApplication()
{
$app = require $this->module->config['bootstrap_file'];
$app->loadEnvironmentFrom($this->module->config['environment_file']);
$app->instance('request', new Request());
return $app;
}
/**
* Replace the Laravel event dispatcher with a mock.
*/
private function mockEventDispatcher()
{
// Even if events are disabled we still want to record the triggered events.
// But by mocking the event dispatcher the wildcard listener registered in the initialize method is removed.
// So to record the triggered events we have to catch the calls to the fire method of the event dispatcher mock.
$callback = function ($event) {
$this->triggeredEvents[] = $this->normalizeEvent($event);
return [];
};
// In Laravel 5.4 the Illuminate\Contracts\Events\Dispatcher interface was changed,
// the 'fire' method was renamed to 'dispatch'. This code determines the correct method to mock.
$method = method_exists($this->app['events'], 'dispatch') ? 'dispatch' : 'fire';
$mock = Stub::makeEmpty('Illuminate\Contracts\Events\Dispatcher', [
$method => $callback
]);
$this->app->instance('events', $mock);
}
/**
* Normalize events to class names.
*
* @param $event
* @return string
*/
private function normalizeEvent($event)
{
if (is_object($event)) {
$event = get_class($event);
}
if (preg_match('/^bootstrapp(ing|ed): /', $event)) {
return $event;
}
// Events can be formatted as 'event.name: parameters'
$segments = explode(':', $event);
return $segments[0];
}
//======================================================================
// Public methods called by module
//======================================================================
/**
* Did an event trigger?
*
* @param $event
* @return bool
*/
public function eventTriggered($event)
{
$event = $this->normalizeEvent($event);
foreach ($this->triggeredEvents as $triggeredEvent) {
if ($event == $triggeredEvent || is_subclass_of($event, $triggeredEvent)) {
return true;
}
}
return false;
}
/**
* Disable Laravel exception handling.
*/
public function disableExceptionHandling()
{
$this->exceptionHandlingDisabled = true;
$this->app['Illuminate\Contracts\Debug\ExceptionHandler']->exceptionHandlingDisabled(true);
}
/**
* Enable Laravel exception handling.
*/
public function enableExceptionHandling()
{
$this->exceptionHandlingDisabled = false;
$this->app['Illuminate\Contracts\Debug\ExceptionHandler']->exceptionHandlingDisabled(false);
}
/**
* Disable events.
*/
public function disableEvents()
{
$this->eventsDisabled = true;
$this->mockEventDispatcher();
}
/**
* Disable model events.
*/
public function disableModelEvents()
{
$this->modelEventsDisabled = true;
Model::unsetEventDispatcher();
}
/*
* Disable middleware.
*/
public function disableMiddleware()
{
$this->middlewareDisabled = true;
$this->app->instance('middleware.disable', true);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Codeception\Lib\Connector\Laravel5;
use Exception;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
/**
* Class ExceptionHandlerDecorator
*
* @package Codeception\Lib\Connector\Laravel5
*/
class ExceptionHandlerDecorator implements ExceptionHandlerContract
{
/**
* @var ExceptionHandlerContract
*/
private $laravelExceptionHandler;
/**
* @var boolean
*/
private $exceptionHandlingDisabled = true;
/**
* ExceptionHandlerDecorator constructor.
*
* @param object $laravelExceptionHandler
*/
public function __construct($laravelExceptionHandler)
{
$this->laravelExceptionHandler = $laravelExceptionHandler;
}
/**
* @param boolean $exceptionHandlingDisabled
*/
public function exceptionHandlingDisabled($exceptionHandlingDisabled)
{
$this->exceptionHandlingDisabled = $exceptionHandlingDisabled;
}
/**
* Report or log an exception.
*
* @param \Exception $e
* @return void
*/
public function report(Exception $e)
{
$this->laravelExceptionHandler->report($e);
}
/**
* @param $request
* @param Exception $e
* @return \Symfony\Component\HttpFoundation\Response
* @throws Exception
*/
public function render($request, Exception $e)
{
$response = $this->laravelExceptionHandler->render($request, $e);
if ($this->exceptionHandlingDisabled && $this->isSymfonyExceptionHandlerOutput($response->getContent())) {
// If content was generated by the \Symfony\Component\Debug\ExceptionHandler class
// the Laravel application could not handle the exception,
// so re-throw this exception if the Codeception user disabled Laravel's exception handling.
throw $e;
}
return $response;
}
/**
* Check if the response content is HTML output of the Symfony exception handler class.
*
* @param string $content
* @return bool
*/
private function isSymfonyExceptionHandlerOutput($content)
{
return strpos($content, '<div id="sf-resetcontent" class="sf-reset">') !== false ||
strpos($content, '<div class="exception-summary">') !== false;
}
/**
* Render an exception to the console.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param \Exception $e
* @return void
*/
public function renderForConsole($output, Exception $e)
{
$this->laravelExceptionHandler->renderForConsole($output, $e);
}
/**
* @param string $method
* @param array $args
* @return mixed
*/
public function __call($method, $args)
{
return call_user_func_array([$this->laravelExceptionHandler, $method], $args);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\Lumen\DummyKernel;
use Codeception\Lib\Connector\Shared\LaravelCommon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Facade;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Client;
use Illuminate\Http\UploadedFile;
class Lumen extends Client
{
use LaravelCommon;
/**
* @var \Laravel\Lumen\Application
*/
private $app;
/**
* @var \Codeception\Module\Lumen
*/
private $module;
/**
* @var bool
*/
private $firstRequest = true;
/**
* @var object
*/
private $oldDb;
/**
* Constructor.
*
* @param \Codeception\Module\Lumen $module
*/
public function __construct($module)
{
$this->module = $module;
$components = parse_url($this->module->config['url']);
$server = ['HTTP_HOST' => $components['host']];
// Pass a DummyKernel to satisfy the arguments of the parent constructor.
// The actual kernel object is set in the initialize() method.
parent::__construct(new DummyKernel(), $server);
// Parent constructor defaults to not following redirects
$this->followRedirects(true);
$this->initialize();
}
/**
* Execute a request.
*
* @param SymfonyRequest $request
* @return Response
*/
protected function doRequest($request)
{
if (!$this->firstRequest) {
$this->initialize($request);
}
$this->firstRequest = false;
$this->applyBindings();
$this->applyContextualBindings();
$this->applyInstances();
$this->applyApplicationHandlers();
$request = Request::createFromBase($request);
$response = $this->kernel->handle($request);
$method = new \ReflectionMethod(get_class($this->app), 'callTerminableMiddleware');
$method->setAccessible(true);
$method->invoke($this->app, $response);
return $response;
}
/**
* Initialize the Lumen framework.
*
* @param SymfonyRequest|null $request
*/
private function initialize($request = null)
{
// Store a reference to the database object
// so the database connection can be reused during tests
$this->oldDb = null;
if (isset($this->app['db']) && $this->app['db']->connection()) {
$this->oldDb = $this->app['db'];
}
if (class_exists(Facade::class)) {
// If the container has been instantiated ever,
// we need to clear its static fields before create new container.
Facade::clearResolvedInstances();
}
$this->app = $this->kernel = require $this->module->config['bootstrap_file'];
// Lumen registers necessary bindings on demand when calling $app->make(),
// so here we force the request binding before registering our own request object,
// otherwise Lumen will overwrite our request object.
$this->app->make('request');
$request = $request ?: SymfonyRequest::create($this->module->config['url']);
$this->app->instance('Illuminate\Http\Request', Request::createFromBase($request));
// Reset the old database if there is one
if ($this->oldDb) {
$this->app->singleton('db', function () {
return $this->oldDb;
});
Model::setConnectionResolver($this->oldDb);
}
$this->module->setApplication($this->app);
}
/**
* Make sure files are \Illuminate\Http\UploadedFile instances with the private $test property set to true.
* Fixes issue https://github.com/Codeception/Codeception/pull/3417.
*
* @param array $files
* @return array
*/
protected function filterFiles(array $files)
{
$files = parent::filterFiles($files);
if (! class_exists('Illuminate\Http\UploadedFile')) {
// The \Illuminate\Http\UploadedFile class was introduced in Laravel 5.2.15,
// so don't change the $files array if it does not exist.
return $files;
}
return $this->convertToTestFiles($files);
}
/**
* @param array $files
* @return array
*/
private function convertToTestFiles(array $files)
{
$filtered = [];
foreach ($files as $key => $value) {
if (is_array($value)) {
$filtered[$key] = $this->convertToTestFiles($value);
} else {
$filtered[$key] = UploadedFile::createFromBase($value, true);
}
}
return $filtered;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Codeception\Lib\Connector\Lumen;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Dummy kernel to satisfy the parent constructor of the LumenConnector class.
*/
class DummyKernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
//
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Codeception\Lib\Connector;
use Closure;
use Phalcon\Di;
use Phalcon\Http;
use RuntimeException;
use ReflectionProperty;
use Codeception\Util\Stub;
use Phalcon\Mvc\Application;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\Client;
use Phalcon\Mvc\Micro as MicroApplication;
use Symfony\Component\BrowserKit\Response;
use Codeception\Lib\Connector\Shared\PhpSuperGlobalsConverter;
class Phalcon extends Client
{
use PhpSuperGlobalsConverter;
/**
* Phalcon Application
* @var mixed
*/
private $application;
/**
* Set Phalcon Application by \Phalcon\DI\Injectable, Closure or bootstrap file path
*
* @param mixed $application
*/
public function setApplication($application)
{
$this->application = $application;
}
/**
* Get Phalcon Application
*
* @return Application|MicroApplication
*/
public function getApplication()
{
$application = $this->application;
if ($application instanceof Closure) {
return $application();
} elseif (is_string($application)) {
/** @noinspection PhpIncludeInspection */
return require $application;
}
return $application;
}
/**
* Makes a request.
*
* @param \Symfony\Component\BrowserKit\Request $request
*
* @return \Symfony\Component\BrowserKit\Response
* @throws \RuntimeException
*/
public function doRequest($request)
{
$application = $this->getApplication();
if (!$application instanceof Application && !$application instanceof MicroApplication) {
throw new RuntimeException('Unsupported application class.');
}
$di = $application->getDI();
/** @var Http\Request $phRequest */
if ($di->has('request')) {
$phRequest = $di->get('request');
}
if (!$phRequest instanceof Http\RequestInterface) {
$phRequest = new Http\Request();
}
$uri = $request->getUri() ?: $phRequest->getURI();
$pathString = parse_url($uri, PHP_URL_PATH);
$queryString = parse_url($uri, PHP_URL_QUERY);
$_SERVER = $request->getServer();
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['REQUEST_URI'] = null === $queryString ? $pathString : $pathString . '?' . $queryString;
$_COOKIE = $request->getCookies();
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = [];
$_GET = [];
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
parse_str($queryString, $output);
foreach ($output as $k => $v) {
$_GET[$k] = $v;
}
$_GET['_url'] = $pathString;
$_SERVER['QUERY_STRING'] = http_build_query($_GET);
Di::reset();
Di::setDefault($di);
$di['request'] = Stub::construct($phRequest, [], ['getRawBody' => $request->getContent()]);
$response = $application->handle();
if (!$response instanceof Http\ResponseInterface) {
$response = $application->response;
}
$headers = $response->getHeaders();
$status = (int) $headers->get('Status');
$headersProperty = new ReflectionProperty($headers, '_headers');
$headersProperty->setAccessible(true);
$headers = $headersProperty->getValue($headers);
if (!is_array($headers)) {
$headers = [];
}
$cookiesProperty = new ReflectionProperty($di['cookies'], '_cookies');
$cookiesProperty->setAccessible(true);
$cookies = $cookiesProperty->getValue($di['cookies']);
if (is_array($cookies)) {
$restoredProperty = new ReflectionProperty('\Phalcon\Http\Cookie', '_restored');
$restoredProperty->setAccessible(true);
$valueProperty = new ReflectionProperty('\Phalcon\Http\Cookie', '_value');
$valueProperty->setAccessible(true);
foreach ($cookies as $name => $cookie) {
if (!$restoredProperty->getValue($cookie)) {
$clientCookie = new Cookie(
$name,
$valueProperty->getValue($cookie),
$cookie->getExpiration(),
$cookie->getPath(),
$cookie->getDomain(),
$cookie->getSecure(),
$cookie->getHttpOnly()
);
$headers['Set-Cookie'][] = (string)$clientCookie;
}
}
}
return new Response(
$response->getContent(),
$status ? $status : 200,
$headers
);
}
}

View File

@@ -0,0 +1,309 @@
<?php
namespace Codeception\Lib\Connector\Phalcon;
use Phalcon\Session\AdapterInterface;
class MemorySession implements AdapterInterface
{
/**
* @var string
*/
protected $sessionId;
/**
* @var string
*/
protected $name;
/**
* @var bool
*/
protected $started = false;
/**
* @var array
*/
protected $memory = [];
/**
* @var array
*/
protected $options = [];
public function __construct(array $options = null)
{
$this->sessionId = $this->generateId();
if (is_array($options)) {
$this->setOptions($options);
}
}
/**
* @inheritdoc
*/
public function start()
{
if ($this->status() !== PHP_SESSION_ACTIVE) {
$this->memory = [];
$this->started = true;
return true;
}
return false;
}
/**
* @inheritdoc
*
* @param array $options
*/
public function setOptions(array $options)
{
if (isset($options['uniqueId'])) {
$this->sessionId = $options['uniqueId'];
}
$this->options = $options;
}
/**
* @inheritdoc
*
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* @inheritdoc
*
* @param string $index
* @param mixed $defaultValue
* @param bool $remove
* @return mixed
*/
public function get($index, $defaultValue = null, $remove = false)
{
$key = $this->prepareIndex($index);
if (!isset($this->memory[$key])) {
return $defaultValue;
}
$return = $this->memory[$key];
if ($remove) {
unset($this->memory[$key]);
}
return $return;
}
/**
* @inheritdoc
*
* @param string $index
* @param mixed $value
*/
public function set($index, $value)
{
$this->memory[$this->prepareIndex($index)] = $value;
}
/**
* @inheritdoc
*
* @param string $index
* @return bool
*/
public function has($index)
{
return isset($this->memory[$this->prepareIndex($index)]);
}
/**
* @inheritdoc
*
* @param string $index
*/
public function remove($index)
{
unset($this->memory[$this->prepareIndex($index)]);
}
/**
* @inheritdoc
*
* @return string
*/
public function getId()
{
return $this->sessionId;
}
/**
* @inheritdoc
*
* @return bool
*/
public function isStarted()
{
return $this->started;
}
/**
* Returns the status of the current session
*
* ``` php
* <?php
* if ($session->status() !== PHP_SESSION_ACTIVE) {
* $session->start();
* }
* ?>
* ```
*
* @return int
*/
public function status()
{
if ($this->isStarted()) {
return PHP_SESSION_ACTIVE;
}
return PHP_SESSION_NONE;
}
/**
* @inheritdoc
*
* @param bool $removeData
* @return bool
*/
public function destroy($removeData = false)
{
if ($removeData) {
if (!empty($this->sessionId)) {
foreach ($this->memory as $key => $value) {
if (0 === strpos($key, $this->sessionId . '#')) {
unset($this->memory[$key]);
}
}
} else {
$this->memory = [];
}
}
$this->started = false;
return true;
}
/**
* @inheritdoc
*
* @param bool $deleteOldSession
* @return \Phalcon\Session\AdapterInterface
*/
public function regenerateId($deleteOldSession = true)
{
$this->sessionId = $this->generateId();
return $this;
}
/**
* @inheritdoc
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @inheritdoc
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Dump all session
*
* @return array
*/
public function toArray()
{
return (array) $this->memory;
}
/**
* Alias: Gets a session variable from an application context
*
* @param string $index
* @return mixed
*/
public function __get($index)
{
return $this->get($index);
}
/**
* Alias: Sets a session variable in an application context
*
* @param string $index
* @param mixed $value
*/
public function __set($index, $value)
{
$this->set($index, $value);
}
/**
* Alias: Check whether a session variable is set in an application context
*
* @param string $index
* @return bool
*/
public function __isset($index)
{
return $this->has($index);
}
/**
* Alias: Removes a session variable from an application context
*
* @param string $index
*/
public function __unset($index)
{
$this->remove($index);
}
private function prepareIndex($index)
{
if ($this->sessionId) {
$key = $this->sessionId . '#' . $index;
} else {
$key = $index;
}
return $key;
}
/**
* @return string
*/
private function generateId()
{
return md5(time());
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Codeception\Lib\Connector\Shared;
/**
* Common functions for Laravel family
*
* @package Codeception\Lib\Connector\Shared
*/
trait LaravelCommon
{
/**
* @var array
*/
private $bindings = [];
/**
* @var array
*/
private $contextualBindings = [];
/**
* @var array
*/
private $instances = [];
/**
* @var array
*/
private $applicationHandlers = [];
/**
* Apply the registered application handlers.
*/
private function applyApplicationHandlers()
{
foreach ($this->applicationHandlers as $handler) {
call_user_func($handler, $this->app);
}
}
/**
* Apply the registered Laravel service container bindings.
*/
private function applyBindings()
{
foreach ($this->bindings as $abstract => $binding) {
list($concrete, $shared) = $binding;
$this->app->bind($abstract, $concrete, $shared);
}
}
/**
* Apply the registered Laravel service container contextual bindings.
*/
private function applyContextualBindings()
{
foreach ($this->contextualBindings as $concrete => $bindings) {
foreach ($bindings as $abstract => $implementation) {
$this->app->addContextualBinding($concrete, $abstract, $implementation);
}
}
}
/**
* Apply the registered Laravel service container instance bindings.
*/
private function applyInstances()
{
foreach ($this->instances as $abstract => $instance) {
$this->app->instance($abstract, $instance);
}
}
//======================================================================
// Public methods called by module
//======================================================================
/**
* Register a Laravel service container binding that should be applied
* after initializing the Laravel Application object.
*
* @param $abstract
* @param $concrete
* @param bool $shared
*/
public function haveBinding($abstract, $concrete, $shared = false)
{
$this->bindings[$abstract] = [$concrete, $shared];
}
/**
* Register a Laravel service container contextual binding that should be applied
* after initializing the Laravel Application object.
*
* @param $concrete
* @param $abstract
* @param $implementation
*/
public function haveContextualBinding($concrete, $abstract, $implementation)
{
if (! isset($this->contextualBindings[$concrete])) {
$this->contextualBindings[$concrete] = [];
}
$this->contextualBindings[$concrete][$abstract] = $implementation;
}
/**
* Register a Laravel service container instance binding that should be applied
* after initializing the Laravel Application object.
*
* @param $abstract
* @param $instance
*/
public function haveInstance($abstract, $instance)
{
$this->instances[$abstract] = $instance;
}
/**
* Register a handler than can be used to modify the Laravel application object after it is initialized.
* The Laravel application object will be passed as an argument to the handler.
*
* @param $handler
*/
public function haveApplicationHandler($handler)
{
$this->applicationHandlers[] = $handler;
}
/**
* Clear the registered application handlers.
*/
public function clearApplicationHandlers()
{
$this->applicationHandlers = [];
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Codeception\Lib\Connector\Shared;
/**
* Converts BrowserKit\Request's request parameters and files into PHP-compatible structure
*
* @see https://bugs.php.net/bug.php?id=25589
* @see https://bugs.php.net/bug.php?id=25589
*
* @package Codeception\Lib\Connector
*/
trait PhpSuperGlobalsConverter
{
/**
* Rearrange files array to be compatible with PHP $_FILES superglobal structure
* @see https://bugs.php.net/bug.php?id=25589
*
* @param array $requestFiles
*
* @return array
*/
protected function remapFiles(array $requestFiles)
{
$files = $this->rearrangeFiles($requestFiles);
return $this->replaceSpaces($files);
}
/**
* Escape high-level variable name with dots, underscores and other "special" chars
* to be compatible with PHP "bug"
* @see https://bugs.php.net/bug.php?id=40000
*
* @param array $parameters
*
* @return array
*/
protected function remapRequestParameters(array $parameters)
{
return $this->replaceSpaces($parameters);
}
private function rearrangeFiles($requestFiles)
{
$files = [];
foreach ($requestFiles as $name => $info) {
if (!is_array($info)) {
continue;
}
/**
* If we have a form with fields like
* ```
* <input type="file" name="foo" />
* <input type="file" name="foo[bar]" />
* ```
* then only array variable will be used while simple variable will be ignored in php $_FILES
* (eg $_FILES = [
* foo => [
* tmp_name => [
* 'bar' => 'asdf'
* ],
* //...
* ]
* ]
* )
* (notice there is no entry for file "foo", only for file "foo[bar]"
* this will check if current element contains inner arrays within it's keys
* so we can ignore element itself and only process inner files
*/
$hasInnerArrays = count(array_filter($info, 'is_array'));
if ($hasInnerArrays || !isset($info['tmp_name'])) {
$inner = $this->remapFiles($info);
foreach ($inner as $innerName => $innerInfo) {
/**
* Convert from ['a' => ['tmp_name' => '/tmp/test.txt'] ]
* to ['tmp_name' => ['a' => '/tmp/test.txt'] ]
*/
$innerInfo = array_map(
function ($v) use ($innerName) {
return [$innerName => $v];
},
$innerInfo
);
if (empty($files[$name])) {
$files[$name] = [];
}
$files[$name] = array_replace_recursive($files[$name], $innerInfo);
}
} else {
$files[$name] = $info;
}
}
return $files;
}
/**
* Replace spaces and dots and other chars in high-level query parameters for
* compatibility with PHP bug (or not a bug)
* @see https://bugs.php.net/bug.php?id=40000
*
* @param array $parameters Array of request parameters to be converted
*
* @return array
*/
private function replaceSpaces($parameters)
{
$qs = http_build_query($parameters, '', '&');
parse_str($qs, $output);
return $output;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Codeception\Lib\Connector;
class Symfony extends \Symfony\Component\HttpKernel\Client
{
/**
* @var boolean
*/
private $rebootable = true;
/**
* @var boolean
*/
private $hasPerformedRequest = false;
/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
private $container = null;
/**
* @var array
*/
public $persistentServices = [];
/**
* Constructor.
*
* @param \Symfony\Component\HttpKernel\Kernel $kernel A booted HttpKernel instance
* @param array $services An injected services
* @param boolean $rebootable
*/
public function __construct(\Symfony\Component\HttpKernel\Kernel $kernel, array $services = [], $rebootable = true)
{
parent::__construct($kernel);
$this->followRedirects(true);
$this->rebootable = (boolean)$rebootable;
$this->persistentServices = $services;
$this->rebootKernel();
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
*/
protected function doRequest($request)
{
if ($this->rebootable) {
if ($this->hasPerformedRequest) {
$this->rebootKernel();
} else {
$this->hasPerformedRequest = true;
}
}
return parent::doRequest($request);
}
/**
* Reboot kernel
*
* Services from the list of persistent services
* are updated from service container before kernel shutdown
* and injected into newly initialized container after kernel boot.
*/
public function rebootKernel()
{
if ($this->container) {
foreach ($this->persistentServices as $serviceName => $service) {
if ($this->container->has($serviceName)) {
$this->persistentServices[$serviceName] = $this->container->get($serviceName);
}
}
}
$this->kernel->shutdown();
$this->kernel->boot();
$this->container = $this->kernel->getContainer();
foreach ($this->persistentServices as $serviceName => $service) {
$this->container->set($serviceName, $service);
}
if ($this->container->has('profiler')) {
$this->container->get('profiler')->enable();
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Codeception\Lib\Connector;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response;
class Universal extends Client
{
use Shared\PhpSuperGlobalsConverter;
protected $mockedResponse;
protected $index;
public function setIndex($index)
{
$this->index = $index;
}
public function mockResponse($response)
{
$this->mockedResponse = $response;
}
public function doRequest($request)
{
if ($this->mockedResponse) {
$response = $this->mockedResponse;
$this->mockedResponse = null;
return $response;
}
$_COOKIE = $request->getCookies();
$_SERVER = $request->getServer();
$_FILES = $this->remapFiles($request->getFiles());
$uri = str_replace('http://localhost', '', $request->getUri());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
if (strtoupper($request->getMethod()) == 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['REQUEST_URI'] = $uri;
ob_start();
include $this->index;
$content = ob_get_contents();
ob_end_clean();
$headers = [];
$php_headers = headers_list();
foreach ($php_headers as $value) {
// Get the header name
$parts = explode(':', $value);
if (count($parts) > 1) {
$name = trim(array_shift($parts));
// Build the header hash map
$headers[$name] = trim(implode(':', $parts));
}
}
$headers['Content-type'] = isset($headers['Content-type'])
? $headers['Content-type']
: "text/html; charset=UTF-8";
$response = new Response($content, 200, $headers);
return $response;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Util\Stub;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response;
class Yii1 extends Client
{
use Shared\PhpSuperGlobalsConverter;
/**
* http://localhost/path/to/your/app/index.php
* @var string url of the entry Yii script
*/
public $url;
/**
* Current application settings {@see Codeception\Module\Yii1::$appSettings}
* @var array
*/
public $appSettings;
/**
* Full path to your application
* @var string
*/
public $appPath;
/**
* Current request headers
* @var array
*/
private $headers;
/**
*
* @param \Symfony\Component\BrowserKit\Request $request
*
* @return \Symfony\Component\BrowserKit\Response
*/
public function doRequest($request)
{
$this->headers = [];
$_COOKIE = array_merge($_COOKIE, $request->getCookies());
$_SERVER = array_merge($_SERVER, $request->getServer());
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = $_GET = [];
if (strtoupper($request->getMethod()) == 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
// Parse url parts
$uriPath = ltrim(parse_url($request->getUri(), PHP_URL_PATH), '/');
$uriQuery = ltrim(parse_url($request->getUri(), PHP_URL_QUERY), '?');
$scriptName = trim(parse_url($this->url, PHP_URL_PATH), '/');
if (!empty($uriQuery)) {
$uriPath .= "?{$uriQuery}";
parse_str($uriQuery, $params);
foreach ($params as $k => $v) {
$_GET[$k] = $v;
}
}
// Add script name to request if none
if ($scriptName and strpos($uriPath, $scriptName) === false) {
$uriPath = "/{$scriptName}/{$uriPath}";
}
// Add forward slash if not exists
if (strpos($uriPath, '/') !== 0) {
$uriPath = "/{$uriPath}";
}
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['REQUEST_URI'] = $uriPath;
/**
* Hack to be sure that CHttpRequest will resolve route correctly
*/
$_SERVER['SCRIPT_NAME'] = "/{$scriptName}";
$_SERVER['SCRIPT_FILENAME'] = $this->appPath;
ob_start();
\Yii::setApplication(null);
\Yii::createApplication($this->appSettings['class'], $this->appSettings['config']);
$app = \Yii::app();
// disabling logging. Logs slow down test execution
if ($app->hasComponent('log')) {
foreach ($app->getComponent('log')->routes as $route) {
$route->enabled = false;
}
}
if ($app->hasComponent('session')) { // disable regenerate id in session
$app->setComponent('session', Stub::make('CHttpSession', ['regenerateID' => false]));
}
$app->onEndRequest->add([$this, 'setHeaders']);
$app->run();
if ($app->hasComponent('db')) {
// close connection
$app->getDb()->setActive(false);
// cleanup metadata cache
$property = new \ReflectionProperty('CActiveRecord', '_md');
$property->setAccessible(true);
$property->setValue([]);
}
$content = ob_get_clean();
$headers = $this->getHeaders();
$statusCode = 200;
foreach ($headers as $header => $val) {
if ($header == 'Location') {
$statusCode = 302;
}
}
$response = new Response($content, $statusCode, $this->getHeaders());
return $response;
}
/**
* Set current client headers when terminating yii application (onEndRequest)
*/
public function setHeaders()
{
$this->headers = \Yii::app()->request->getAllHeaders();
}
/**
* Returns current client headers
* @return array headers
*/
public function getHeaders()
{
return $this->headers;
}
}

View File

@@ -0,0 +1,396 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\Yii2\Logger;
use Codeception\Lib\InnerBrowser;
use Codeception\Util\Debug;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\Response;
use Yii;
use yii\base\ExitException;
use yii\base\Security;
use yii\web\Application;
use yii\web\ErrorHandler;
use yii\web\HttpException;
use yii\web\Request;
use yii\web\Response as YiiResponse;
class Yii2 extends Client
{
use Shared\PhpSuperGlobalsConverter;
const CLEAN_METHODS = [
self::CLEAN_RECREATE,
self::CLEAN_CLEAR,
self::CLEAN_FORCE_RECREATE,
self::CLEAN_MANUAL
];
/**
* Clean the response object by recreating it.
* This might lose behaviors / event handlers / other changes that are done in the application bootstrap phase.
*/
const CLEAN_RECREATE = 'recreate';
/**
* Same as recreate but will not warn when behaviors / event handlers are lost.
*/
const CLEAN_FORCE_RECREATE = 'force_recreate';
/**
* Clean the response object by resetting specific properties via its' `clear()` method.
* This will keep behaviors / event handlers, but could inadvertently leave some changes intact.
* @see \Yii\web\Response::clear()
*/
const CLEAN_CLEAR = 'clear';
/**
* Do not clean the response, instead the test writer will be responsible for manually resetting the response in
* between requests during one test
*/
const CLEAN_MANUAL = 'manual';
/**
* @var string application config file
*/
public $configFile;
/**
* @var string method for cleaning the response object before each request
*/
public $responseCleanMethod;
/**
* @var string method for cleaning the request object before each request
*/
public $requestCleanMethod;
/**
* @var string[] List of component names that must be recreated before each request
*/
public $recreateComponents = [];
/**
* This option is there primarily for backwards compatibility.
* It means you cannot make any modification to application state inside your app, since they will get discarded.
* @var bool whether to recreate the whole application before each request
*/
public $recreateApplication = false;
/**
* @return \yii\web\Application
*/
public function getApplication()
{
if (!isset(Yii::$app)) {
$this->startApp();
}
return Yii::$app;
}
public function resetApplication()
{
codecept_debug('Destroying application');
Yii::$app = null;
\yii\web\UploadedFile::reset();
if (method_exists(\yii\base\Event::className(), 'offAll')) {
\yii\base\Event::offAll();
}
Yii::setLogger(null);
// This resolves an issue with database connections not closing properly.
gc_collect_cycles();
}
public function startApp()
{
codecept_debug('Starting application');
$config = require($this->configFile);
if (!isset($config['class'])) {
$config['class'] = 'yii\web\Application';
}
$config = $this->mockMailer($config);
/** @var \yii\web\Application $app */
Yii::$app = Yii::createObject($config);
Yii::setLogger(new Logger());
}
/**
*
* @param \Symfony\Component\BrowserKit\Request $request
*
* @return \Symfony\Component\BrowserKit\Response
*/
public function doRequest($request)
{
$_COOKIE = $request->getCookies();
$_SERVER = $request->getServer();
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = $_GET = [];
if (strtoupper($request->getMethod()) === 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
$uri = $request->getUri();
$pathString = parse_url($uri, PHP_URL_PATH);
$queryString = parse_url($uri, PHP_URL_QUERY);
$_SERVER['REQUEST_URI'] = $queryString === null ? $pathString : $pathString . '?' . $queryString;
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
parse_str($queryString, $params);
foreach ($params as $k => $v) {
$_GET[$k] = $v;
}
ob_start();
$this->beforeRequest();
$app = $this->getApplication();
// disabling logging. Logs are slowing test execution down
foreach ($app->log->targets as $target) {
$target->enabled = false;
}
$yiiRequest = $app->getRequest();
if ($request->getContent() !== null) {
$yiiRequest->setRawBody($request->getContent());
$yiiRequest->setBodyParams(null);
} else {
$yiiRequest->setRawBody(null);
$yiiRequest->setBodyParams($_POST);
}
$yiiRequest->setQueryParams($_GET);
try {
/*
* This is basically equivalent to $app->run() without sending the response.
* Sending the response is problematic because it tries to send headers.
*/
$app->trigger($app::EVENT_BEFORE_REQUEST);
$response = $app->handleRequest($yiiRequest);
$app->trigger($app::EVENT_AFTER_REQUEST);
$response->send();
} catch (\Exception $e) {
if ($e instanceof HttpException) {
// Don't discard output and pass exception handling to Yii to be able
// to expect error response codes in tests.
$app->errorHandler->discardExistingOutput = false;
$app->errorHandler->handleException($e);
} elseif (!$e instanceof ExitException) {
// for exceptions not related to Http, we pass them to Codeception
throw $e;
}
$response = $app->response;
}
$this->encodeCookies($response, $yiiRequest, $app->security);
if ($response->isRedirection) {
Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true));
}
$content = ob_get_clean();
if (empty($content) && !empty($response->content)) {
throw new \Exception('No content was sent from Yii application');
}
return new Response($content, $response->statusCode, $response->getHeaders()->toArray());
}
protected function revertErrorHandler()
{
$handler = new ErrorHandler();
set_error_handler([$handler, 'errorHandler']);
}
/**
* Encodes the cookies and adds them to the headers.
* @param \yii\web\Response $response
* @throws \yii\base\InvalidConfigException
*/
protected function encodeCookies(
YiiResponse $response,
Request $request,
Security $security
) {
if ($request->enableCookieValidation) {
$validationKey = $request->cookieValidationKey;
}
foreach ($response->getCookies() as $cookie) {
/** @var \yii\web\Cookie $cookie */
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
$data = version_compare(Yii::getVersion(), '2.0.2', '>')
? [$cookie->name, $cookie->value]
: $cookie->value;
$value = $security->hashData(serialize($data), $validationKey);
}
$c = new Cookie(
$cookie->name,
$value,
$cookie->expire,
$cookie->path,
$cookie->domain,
$cookie->secure,
$cookie->httpOnly
);
$this->getCookieJar()->set($c);
}
}
/**
* Replace mailer with in memory mailer
* @param array $config Original configuration
* @return array New configuration
*/
protected function mockMailer(array $config)
{
// options that make sense for mailer mock
$allowedOptions = [
'htmlLayout',
'textLayout',
'messageConfig',
'messageClass',
'useFileTransport',
'fileTransportPath',
'fileTransportCallback',
'view',
'viewPath',
];
$mailerConfig = [
'class' => 'Codeception\Lib\Connector\Yii2\TestMailer',
];
if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) {
foreach ($config['components']['mailer'] as $name => $value) {
if (in_array($name, $allowedOptions, true)) {
$mailerConfig[$name] = $value;
}
}
}
$config['components']['mailer'] = $mailerConfig;
return $config;
}
public function restart()
{
parent::restart();
$this->resetApplication();
}
/**
* Resets the applications' response object.
* The method used depends on the module configuration.
*/
protected function resetResponse(Application $app)
{
$method = $this->responseCleanMethod;
// First check the current response object.
if (($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND)
|| $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND)
|| $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE)
|| count($app->response->getBehaviors()) > 0
) && $method === self::CLEAN_RECREATE
) {
Debug::debug(<<<TEXT
[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
the response object, this means any behaviors or events that are not attached in the component config will be lost.
We will fall back to clearing the response. If you are certain you want to recreate it, please configure
responseCleanMethod = 'force_recreate' in the module.
TEXT
);
$method = self::CLEAN_CLEAR;
}
switch ($method) {
case self::CLEAN_FORCE_RECREATE:
case self::CLEAN_RECREATE:
$app->set('response', $app->getComponents()['response']);
break;
case self::CLEAN_CLEAR:
$app->response->clear();
break;
case self::CLEAN_MANUAL:
break;
}
}
protected function resetRequest(Application $app)
{
$method = $this->requestCleanMethod;
$request = $app->request;
// First check the current request object.
if (count($request->getBehaviors()) > 0 && $method === self::CLEAN_RECREATE) {
Debug::debug(<<<TEXT
[WARNING] You are attaching event handlers or behaviors to the request object. But the Yii2 module is configured to recreate
the request object, this means any behaviors or events that are not attached in the component config will be lost.
We will fall back to clearing the request. If you are certain you want to recreate it, please configure
requestCleanMethod = 'force_recreate' in the module.
TEXT
);
$method = self::CLEAN_CLEAR;
}
switch ($method) {
case self::CLEAN_FORCE_RECREATE:
case self::CLEAN_RECREATE:
$app->set('request', $app->getComponents()['request']);
break;
case self::CLEAN_CLEAR:
$request->getHeaders()->removeAll();
$request->setBaseUrl(null);
$request->setHostInfo(null);
$request->setPathInfo(null);
$request->setScriptFile(null);
$request->setScriptUrl(null);
$request->setUrl(null);
$request->setPort(null);
$request->setSecurePort(null);
$request->setAcceptableContentTypes(null);
$request->setAcceptableLanguages(null);
break;
case self::CLEAN_MANUAL:
break;
}
}
/**
* Called before each request, preparation happens here.
*/
protected function beforeRequest()
{
if ($this->recreateApplication) {
$this->resetApplication();
return;
}
$application = $this->getApplication();
$this->resetResponse($application);
$this->resetRequest($application);
$definitions = $application->getComponents(true);
foreach ($this->recreateComponents as $component) {
// Only recreate if it has actually been instantiated.
if ($application->has($component, true)) {
$application->set($component, $definitions[$component]);
}
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\base\Event;
use yii\db\Connection;
/**
* Class ConnectionWatcher
* This class will watch for new database connection and store a reference to the connection object.
* @package Codeception\Lib\Connector\Yii2
*/
class ConnectionWatcher
{
private $handler;
/** @var Connection[] */
private $connections = [];
public function __construct()
{
$this->handler = function (Event $event) {
if ($event->sender instanceof Connection) {
$this->connectionOpened($event->sender);
}
};
}
protected function connectionOpened(Connection $connection)
{
$this->debug('Connection opened!');
if ($connection instanceof Connection) {
$this->connections[] = $connection;
}
}
public function start()
{
Event::on(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler);
$this->debug('watching new connections');
}
public function stop()
{
Event::off(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler);
$this->debug('no longer watching new connections');
}
public function closeAll()
{
$count = count($this->connections);
$this->debug("closing all ($count) connections");
foreach ($this->connections as $connection) {
$connection->close();
}
}
protected function debug($message)
{
$title = (new \ReflectionClass($this))->getShortName();
if (is_array($message) or is_object($message)) {
$message = stripslashes(json_encode($message));
}
codecept_debug("[$title] $message");
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\test\FixtureTrait;
use yii\test\InitDbFixture;
class FixturesStore
{
use FixtureTrait;
protected $data;
/**
* Expects fixtures config
*
* FixturesStore constructor.
* @param $data
*/
public function __construct($data)
{
$this->data = $data;
}
public function fixtures()
{
return $this->data;
}
public function globalFixtures()
{
return [
InitDbFixture::className()
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use Codeception\Util\Debug;
class Logger extends \yii\log\Logger
{
public function init()
{
// overridden to prevent register_shutdown_function
}
public function log($message, $level, $category = 'application')
{
if (!in_array($level, [
\yii\log\Logger::LEVEL_INFO,
\yii\log\Logger::LEVEL_WARNING,
\yii\log\Logger::LEVEL_ERROR,
])) {
return;
}
if (strpos($category, 'yii\db\Command')===0) {
return; // don't log queries
}
// https://github.com/Codeception/Codeception/issues/3696
if ($message instanceof \yii\base\Exception) {
$message = $message->__toString();
}
Debug::debug("[$category] " . \yii\helpers\VarDumper::export($message));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\mail\BaseMailer;
class TestMailer extends BaseMailer
{
public $messageClass = 'yii\swiftmailer\Message';
private $sentMessages = [];
protected function sendMessage($message)
{
$this->sentMessages[] = $message;
return true;
}
protected function saveMessage($message)
{
return $this->sendMessage($message);
}
public function getSentMessages()
{
return $this->sentMessages;
}
public function reset()
{
$this->sentMessages = [];
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\base\Event;
use yii\db\Connection;
use yii\db\Transaction;
/**
* Class TransactionForcer
* This class adds support for forcing transactions as well as reusing PDO objects.
* @package Codeception\Lib\Connector\Yii2
*/
class TransactionForcer extends ConnectionWatcher
{
private $ignoreCollidingDSN;
private $pdoCache = [];
private $dsnCache;
private $transactions = [];
public function __construct(
$ignoreCollidingDSN
) {
parent::__construct();
$this->ignoreCollidingDSN = $ignoreCollidingDSN;
}
protected function connectionOpened(Connection $connection)
{
parent::connectionOpened($connection);
/**
* We should check if the known PDO objects are the same, in which case we should reuse the PDO
* object so only 1 transaction is started and multiple connections to the same database see the
* same data (due to writes inside a transaction not being visible from the outside).
*
*/
$key = md5(json_encode([
'dsn' => $connection->dsn,
'user' => $connection->username,
'pass' => $connection->password,
'attributes' => $connection->attributes,
'emulatePrepare' => $connection->emulatePrepare,
'charset' => $connection->charset
]));
/*
* If keys match we assume connections are "similar enough".
*/
if (isset($this->pdoCache[$key])) {
$connection->pdo = $this->pdoCache[$key];
} else {
$this->pdoCache[$key] = $connection->pdo;
}
if (isset($this->dsnCache[$connection->dsn])
&& $this->dsnCache[$connection->dsn] !== $key
&& !$this->ignoreCollidingDSN
) {
$this->debug(<<<TEXT
You use multiple connections to the same DSN ({$connection->dsn}) with different configuration.
These connections will not see the same database state since we cannot share a transaction between different PDO
instances.
You can remove this message by adding 'ignoreCollidingDSN = true' in the module configuration.
TEXT
);
Debug::pause();
}
if (isset($this->transactions[$key])) {
$this->debug('Reusing PDO, so no need for a new transaction');
return;
}
$this->debug('Transaction started for: ' . $connection->dsn);
$this->transactions[$key] = $connection->beginTransaction();
}
public function rollbackAll()
{
/** @var Transaction $transaction */
foreach ($this->transactions as $transaction) {
if ($transaction->db->isActive) {
$transaction->rollBack();
$this->debug('Transaction cancelled; all changes reverted.');
}
}
$this->transactions = [];
$this->pdoCache = [];
$this->dsnCache = [];
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Codeception\Lib\Connector;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
class ZF1 extends Client
{
use Shared\PhpSuperGlobalsConverter;
/**
* @var \Zend_Controller_Front
*/
protected $front;
/**
* @var \Zend_Application
*/
protected $bootstrap;
/**
* @var \Zend_Controller_Request_HttpTestCase
*/
protected $zendRequest;
public function setBootstrap($bootstrap)
{
$this->bootstrap = $bootstrap;
$this->front = $this->bootstrap
->getBootstrap()
->getResource('frontcontroller');
$this->front
->throwExceptions(true)
->returnResponse(false);
}
public function doRequest($request)
{
// redirector should not exit
$redirector = \Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$redirector->setExit(false);
// json helper should not exit
$json = \Zend_Controller_Action_HelperBroker::getStaticHelper('json');
$json->suppressExit = true;
$zendRequest = new \Zend_Controller_Request_HttpTestCase();
$zendRequest->setMethod($request->getMethod());
$zendRequest->setCookies($request->getCookies());
$zendRequest->setParams($request->getParameters());
// Sf2's BrowserKit does not distinguish between GET, POST, PUT etc.,
// so we set all parameters in ZF's request here to not break apps
// relying on $request->getPost()
$zendRequest->setPost($request->getParameters());
$zendRequest->setRawBody($request->getContent());
$uri = $request->getUri();
$queryString = parse_url($uri, PHP_URL_QUERY);
$requestUri = parse_url($uri, PHP_URL_PATH);
if (!empty($queryString)) {
$requestUri .= '?' . $queryString;
}
$zendRequest->setRequestUri($requestUri);
$zendRequest->setHeaders($this->extractHeaders($request));
$_FILES = $this->remapFiles($request->getFiles());
$_SERVER = array_merge($_SERVER, $request->getServer());
$zendResponse = new \Zend_Controller_Response_HttpTestCase;
$this->front->setRequest($zendRequest)->setResponse($zendResponse);
ob_start();
try {
$this->bootstrap->run();
$_GET = $_POST = [];
} catch (\Exception $e) {
ob_end_clean();
$_GET = $_POST = [];
throw $e;
}
ob_end_clean();
$this->zendRequest = $zendRequest;
$response = new Response(
$zendResponse->getBody(),
$zendResponse->getHttpResponseCode(),
$this->formatResponseHeaders($zendResponse)
);
return $response;
}
/**
* Format up the ZF1 response headers into Symfony\Component\BrowserKit\Response headers format.
*
* @param \Zend_Controller_Response_Abstract $response The ZF1 Response Object.
* @return array the clean key/value headers
*/
private function formatResponseHeaders(\Zend_Controller_Response_Abstract $response)
{
$headers = [];
foreach ($response->getHeaders() as $header) {
$name = $header['name'];
if (array_key_exists($name, $headers)) {
if ($header['replace']) {
$headers[$name] = $header['value'];
}
} else {
$headers[$name] = $header['value'];
}
}
return $headers;
}
/**
* @return \Zend_Controller_Request_HttpTestCase
*/
public function getZendRequest()
{
return $this->zendRequest;
}
private function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Connector\ZF2\PersistentServiceManager;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response;
use Zend\Http\Request as HttpRequest;
use Zend\Http\Headers as HttpHeaders;
use Zend\Mvc\Application;
use Zend\Stdlib\Parameters;
use Zend\Uri\Http as HttpUri;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
class ZF2 extends Client
{
/**
* @var \Zend\Mvc\ApplicationInterface
*/
protected $application;
/**
* @var array
*/
protected $applicationConfig;
/**
* @var \Zend\Http\PhpEnvironment\Request
*/
protected $zendRequest;
/**
* @var PersistentServiceManager
*/
private $persistentServiceManager;
/**
* @param array $applicationConfig
*/
public function setApplicationConfig($applicationConfig)
{
$this->applicationConfig = $applicationConfig;
$this->createApplication();
}
/**
* @param Request $request
*
* @return Response
* @throws \Exception
*/
public function doRequest($request)
{
$this->createApplication();
$zendRequest = $this->application->getRequest();
$uri = new HttpUri($request->getUri());
$queryString = $uri->getQuery();
$method = strtoupper($request->getMethod());
$zendRequest->setCookies(new Parameters($request->getCookies()));
$query = [];
$post = [];
$content = $request->getContent();
if ($queryString) {
parse_str($queryString, $query);
}
if ($method !== HttpRequest::METHOD_GET) {
$post = $request->getParameters();
}
$zendRequest->setQuery(new Parameters($query));
$zendRequest->setPost(new Parameters($post));
$zendRequest->setFiles(new Parameters($request->getFiles()));
$zendRequest->setContent($content);
$zendRequest->setMethod($method);
$zendRequest->setUri($uri);
$requestUri = $uri->getPath();
if (!empty($queryString)) {
$requestUri .= '?' . $queryString;
}
$zendRequest->setRequestUri($requestUri);
$zendRequest->setHeaders($this->extractHeaders($request));
$this->application->run();
// get the response *after* the application has run, because other ZF
// libraries like API Agility may *replace* the application's response
//
$zendResponse = $this->application->getResponse();
$this->zendRequest = $zendRequest;
$exception = $this->application->getMvcEvent()->getParam('exception');
if ($exception instanceof \Exception) {
throw $exception;
}
$response = new Response(
$zendResponse->getBody(),
$zendResponse->getStatusCode(),
$zendResponse->getHeaders()->toArray()
);
return $response;
}
/**
* @return \Zend\Http\PhpEnvironment\Request
*/
public function getZendRequest()
{
return $this->zendRequest;
}
private function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
$zendHeaders = new HttpHeaders();
$zendHeaders->addHeaders($headers);
return $zendHeaders;
}
public function grabServiceFromContainer($service)
{
$serviceManager = $this->application->getServiceManager();
if (!$serviceManager->has($service)) {
throw new \PHPUnit\Framework\AssertionFailedError("Service $service is not available in container");
}
if ($service === 'Doctrine\ORM\EntityManager' && !isset($this->persistentServiceManager)) {
if (!method_exists($serviceManager, 'addPeeringServiceManager')) {
throw new ModuleException('Codeception\Module\ZF2', 'integration with Doctrine2 module is not compatible with ZF3');
}
$this->persistentServiceManager = new PersistentServiceManager($serviceManager);
}
return $serviceManager->get($service);
}
public function addServiceToContainer($name, $service)
{
if (!isset($this->persistentServiceManager)) {
$serviceManager = $this->application->getServiceManager();
if (!method_exists($serviceManager, 'addPeeringServiceManager')) {
throw new ModuleException('Codeception\Module\ZF2', 'addServiceToContainer method is not compatible with ZF3');
}
$this->persistentServiceManager = new PersistentServiceManager($serviceManager);
$serviceManager->addPeeringServiceManager($this->persistentServiceManager);
$serviceManager->setRetrieveFromPeeringManagerFirst(true);
}
$this->persistentServiceManager->setAllowOverride(true);
$this->persistentServiceManager->setService($name, $service);
$this->persistentServiceManager->setAllowOverride(false);
}
private function createApplication()
{
$this->application = Application::init($this->applicationConfig);
$serviceManager = $this->application->getServiceManager();
if (isset($this->persistentServiceManager)) {
$serviceManager->addPeeringServiceManager($this->persistentServiceManager);
$serviceManager->setRetrieveFromPeeringManagerFirst(true);
}
$sendResponseListener = $serviceManager->get('SendResponseListener');
$events = $this->application->getEventManager();
if (class_exists('Zend\EventManager\StaticEventManager')) {
$events->detach($sendResponseListener); //ZF2
} else {
$events->detach([$sendResponseListener, 'sendResponse']); //ZF3
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Codeception\Lib\Connector\ZF2;
use \Zend\ServiceManager\ServiceLocatorInterface;
use \Zend\ServiceManager\ServiceManager;
class PersistentServiceManager extends ServiceManager implements ServiceLocatorInterface
{
/**
* @var ServiceLocatorInterface Used to retrieve Doctrine services
*/
private $serviceManager;
public function __construct(ServiceLocatorInterface $serviceManager)
{
$this->serviceManager = $serviceManager;
}
public function get($name, $usePeeringServiceManagers = true)
{
if (parent::has($name)) {
return parent::get($name, $usePeeringServiceManagers);
}
return $this->serviceManager->get($name);
}
public function has($name, $checkAbstractFactories = true, $usePeeringServiceManagers = true)
{
if (parent::has($name, $checkAbstractFactories, $usePeeringServiceManagers)) {
return true;
}
if (preg_match('/doctrine/i', $name)) {
return $this->serviceManager->has($name);
}
return false;
}
public function setService($name, $service)
{
parent::setService($name, $service);
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\ZendExpressive\ResponseCollector;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
use Zend\Diactoros\ServerRequest;
use Zend\Expressive\Application;
use Zend\Diactoros\UploadedFile;
class ZendExpressive extends Client
{
/**
* @var Application
*/
protected $application;
/**
* @var ResponseCollector
*/
protected $responseCollector;
/**
* @param Application
*/
public function setApplication(Application $application)
{
$this->application = $application;
}
/**
* @param ResponseCollector $responseCollector
*/
public function setResponseCollector(ResponseCollector $responseCollector)
{
$this->responseCollector = $responseCollector;
}
/**
* @param Request $request
*
* @return Response
* @throws \Exception
*/
public function doRequest($request)
{
$inputStream = fopen('php://memory', 'r+');
$content = $request->getContent();
if ($content !== null) {
fwrite($inputStream, $content);
rewind($inputStream);
}
$queryParams = [];
$postParams = [];
$queryString = parse_url($request->getUri(), PHP_URL_QUERY);
if ($queryString != '') {
parse_str($queryString, $queryParams);
}
if ($request->getMethod() !== 'GET') {
$postParams = $request->getParameters();
}
$serverParams = $request->getServer();
if (!isset($serverParams['SCRIPT_NAME'])) {
//required by WhoopsErrorHandler
$serverParams['SCRIPT_NAME'] = 'Codeception';
}
$zendRequest = new ServerRequest(
$serverParams,
$this->convertFiles($request->getFiles()),
$request->getUri(),
$request->getMethod(),
$inputStream,
$this->extractHeaders($request)
);
$zendRequest = $zendRequest->withCookieParams($request->getCookies())
->withQueryParams($queryParams)
->withParsedBody($postParams);
$cwd = getcwd();
chdir(codecept_root_dir());
$this->application->run($zendRequest);
chdir($cwd);
$this->request = $zendRequest;
$response = $this->responseCollector->getResponse();
$this->responseCollector->clearResponse();
return new Response(
$response->getBody(),
$response->getStatusCode(),
$response->getHeaders()
);
}
private function convertFiles(array $files)
{
$fileObjects = [];
foreach ($files as $fieldName => $file) {
if ($file instanceof UploadedFile) {
$fileObjects[$fieldName] = $file;
} elseif (!isset($file['tmp_name']) && !isset($file['name'])) {
$fileObjects[$fieldName] = $this->convertFiles($file);
} else {
$fileObjects[$fieldName] = new UploadedFile(
$file['tmp_name'],
$file['size'],
$file['error'],
$file['name'],
$file['type']
);
}
}
return $fileObjects;
}
private function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Codeception\Lib\Connector\ZendExpressive;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\EmitterInterface;
class ResponseCollector implements EmitterInterface
{
/**
* @var ResponseInterface
*/
private $response;
public function emit(ResponseInterface $response)
{
$this->response = $response;
}
public function getResponse()
{
if ($this->response === null) {
throw new \LogicException('Response wasn\'t emitted yet');
}
return $this->response;
}
public function clearResponse()
{
$this->response = null;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Codeception\Lib\Console;
use Symfony\Component\Console\Formatter\OutputFormatter;
class Colorizer
{
/**
* @param string $string
* @return string
*/
public function colorize($string = '')
{
$fp = fopen('php://memory', 'r+');
fwrite($fp, $string);
rewind($fp);
$colorizedMessage = '';
while ($line = fgets($fp)) {
$char = $line[0];
$line = OutputFormatter::escape(trim($line));
switch ($char) {
case '+':
$line = "<info>$line</info>";
break;
case '-':
$line = "<comment>$line</comment>";
break;
}
$colorizedMessage .= $line . "\n";
}
return trim($colorizedMessage);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Codeception\Lib\Console;
use SebastianBergmann\Comparator\ComparisonFailure;
use SebastianBergmann\Diff\Differ;
/**
* DiffFactory
**/
class DiffFactory
{
/**
* @param ComparisonFailure $failure
* @return string|null
*/
public function createDiff(ComparisonFailure $failure)
{
$diff = $this->getDiff($failure->getExpectedAsString(), $failure->getActualAsString());
if (!$diff) {
return null;
}
return $diff;
}
/**
* @param string $expected
* @param string $actual
* @return string
*/
private function getDiff($expected = '', $actual = '')
{
if (!$actual && !$expected) {
return '';
}
$differ = new Differ('');
return $differ->diff($expected, $actual);
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Codeception\Lib\Console;
use Symfony\Component\Console\Output\OutputInterface;
class Message
{
protected $output;
protected $message;
public function __construct($message, Output $output = null)
{
$this->message = $message;
$this->output = $output;
}
public function with($param)
{
$args = array_merge([$this->message], func_get_args());
$this->message = call_user_func_array('sprintf', $args);
return $this;
}
public function style($name)
{
$this->message = sprintf('<%s>%s</%s>', $name, $this->message, $name);
return $this;
}
public function width($length, $char = ' ')
{
$message_length = $this->getLength();
if ($message_length < $length) {
$this->message .= str_repeat($char, $length - $message_length);
}
return $this;
}
public function cut($length)
{
$this->message = mb_substr($this->message, 0, $length, 'utf-8');
return $this;
}
public function write($verbose = OutputInterface::VERBOSITY_NORMAL)
{
if ($verbose > $this->output->getVerbosity()) {
return;
}
$this->output->write($this->message);
}
public function writeln($verbose = OutputInterface::VERBOSITY_NORMAL)
{
if ($verbose > $this->output->getVerbosity()) {
return;
}
$this->output->writeln($this->message);
}
public function prepend($string)
{
if ($string instanceof Message) {
$string = $string->getMessage();
}
$this->message = $string . $this->message;
return $this;
}
public function append($string)
{
if ($string instanceof Message) {
$string = $string->getMessage();
}
$this->message .= $string;
return $this;
}
public function apply($func)
{
$this->message = call_user_func($func, $this->message);
return $this;
}
public function center($char)
{
$this->message = $char . $this->message . $char;
return $this;
}
/**
* @return mixed
*/
public function getMessage()
{
return $this->message;
}
public function block($style)
{
$this->message = $this->output->formatHelper->formatBlock($this->message, $style, true);
return $this;
}
public function getLength($includeTags = false)
{
return mb_strwidth($includeTags ? $this->message : strip_tags($this->message), 'utf-8');
}
public static function ucfirst($text)
{
return mb_strtoupper(mb_substr($text, 0, 1, 'utf-8'), 'utf-8') . mb_substr($text, 1, null, 'utf-8');
}
public function __toString()
{
return $this->message;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Codeception\Lib\Console;
use SebastianBergmann\Comparator\ComparisonFailure;
/**
* MessageFactory
**/
class MessageFactory
{
/**
* @var DiffFactory
*/
protected $diffFactory;
/**
* @var Output
*/
private $output;
/**
* @var Colorizer
*/
protected $colorizer;
/**
* MessageFactory constructor.
* @param Output $output
*/
public function __construct(Output $output)
{
$this->output = $output;
$this->diffFactory = new DiffFactory();
$this->colorizer = new Colorizer();
}
/**
* @param string $text
* @return Message
*/
public function message($text = '')
{
return new Message($text, $this->output);
}
/**
* @param ComparisonFailure $failure
* @return string
*/
public function prepareComparisonFailureMessage(ComparisonFailure $failure)
{
$diff = $this->diffFactory->createDiff($failure);
if (!$diff) {
return '';
}
$diff = $this->colorizer->colorize($diff);
return "\n<comment>- Expected</comment> | <info>+ Actual</info>\n$diff";
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Codeception\Lib\Console;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Output\ConsoleOutput;
class Output extends ConsoleOutput
{
protected $config = [
'colors' => true,
'verbosity' => self::VERBOSITY_NORMAL,
'interactive' => true
];
/**
* @var \Symfony\Component\Console\Helper\FormatterHelper
*/
public $formatHelper;
public $waitForDebugOutput = true;
protected $isInteractive = false;
public function __construct($config)
{
$this->config = array_merge($this->config, $config);
// enable interactive output mode for CLI
$this->isInteractive = $this->config['interactive']
&& isset($_SERVER['TERM'])
&& php_sapi_name() == 'cli'
&& $_SERVER['TERM'] != 'linux';
$formatter = new OutputFormatter($this->config['colors']);
$formatter->setStyle('default', new OutputFormatterStyle());
$formatter->setStyle('bold', new OutputFormatterStyle(null, null, ['bold']));
$formatter->setStyle('focus', new OutputFormatterStyle('magenta', null, ['bold']));
$formatter->setStyle('ok', new OutputFormatterStyle('green', null, ['bold']));
$formatter->setStyle('error', new OutputFormatterStyle('white', 'red', ['bold']));
$formatter->setStyle('fail', new OutputFormatterStyle('red', null, ['bold']));
$formatter->setStyle('pending', new OutputFormatterStyle('yellow', null, ['bold']));
$formatter->setStyle('debug', new OutputFormatterStyle('cyan'));
$formatter->setStyle('comment', new OutputFormatterStyle('yellow'));
$formatter->setStyle('info', new OutputFormatterStyle('green'));
$this->formatHelper = new FormatterHelper();
parent::__construct($this->config['verbosity'], $this->config['colors'], $formatter);
}
public function isInteractive()
{
return $this->isInteractive;
}
protected function clean($message)
{
// clear json serialization
$message = str_replace('\/', '/', $message);
return $message;
}
public function debug($message)
{
$message = print_r($message, true);
$message = str_replace("\n", "\n ", $message);
$message = $this->clean($message);
$message = OutputFormatter::escape($message);
if ($this->waitForDebugOutput) {
$this->writeln('');
$this->waitForDebugOutput = false;
}
$this->writeln("<debug> $message</debug>");
}
public function message($message)
{
$message = call_user_func_array('sprintf', func_get_args());
return new Message($message, $this);
}
public function exception(\Exception $e)
{
$class = get_class($e);
$this->writeln("");
$this->writeln("(![ $class ]!)");
$this->writeln($e->getMessage());
$this->writeln("");
}
public function notification($message)
{
$this->writeln("<comment>$message</comment>");
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Codeception\Lib;
/**
* Populates a db using a parameterized command built from the Db module configuration.
*/
class DbPopulator
{
/**
* The command to be executed.
*
* @var string
*/
private $builtCommand;
/**
* @var array
*/
protected $config;
/**
* Constructs a DbPopulator object for the given command and Db module.
*
* @param $config
* @internal param string $command The parameterized command to evaluate and execute later.
* @internal param Codeception\Module\Db|null $dbModule The Db module used to build the populator command or null.
*/
public function __construct($config)
{
$this->config = $config;
$command = $this->config['populator'];
$this->builtCommand = $this->buildCommand((string) $command);
}
/**
* Builds out a command replacing any found `$key` with its value if found in the given configuration.
*
* Process any $key found in the configuration array as a key of the array and replaces it with
* the found value for the key. Example:
*
* ```php
* <?php
*
* $command = 'Hello $name';
* $config = ['name' => 'Mauro'];
*
* // With the above parameters it will return `'Hello Mauro'`.
* ```
*
* @param string $command The command to be evaluated using the given config
* @param array $config The configuration values used to replace any found $keys with values from this array.
* @return string The resulting command string after evaluating any configuration's key
*/
protected function buildCommand($command)
{
$dsn = isset($this->config['dsn']) ? $this->config['dsn'] : '';
$dsnVars = [];
$dsnWithoutDriver = preg_replace('/^[a-z]+:/i', '', $dsn);
foreach (explode(';', $dsnWithoutDriver) as $item) {
$keyValueTuple = explode('=', $item);
if (count($keyValueTuple) > 1) {
list($k, $v) = array_values($keyValueTuple);
$dsnVars[$k] = $v;
}
}
$vars = array_merge($dsnVars, $this->config);
foreach ($vars as $key => $value) {
$vars['$'.$key] = $value;
unset($vars[$key]);
}
return str_replace(array_keys($vars), array_values($vars), $command);
}
/**
* Executes the command built using the Db module configuration.
*
* Uses the PHP `exec` to spin off a child process for the built command.
*
* @return bool
*/
public function run()
{
$command = $this->getBuiltCommand();
codecept_debug("[Db] Executing Populator: `$command`");
exec($command, $output, $exitCode);
if (0 !== $exitCode) {
throw new \RuntimeException(
"The populator command did not end successfully: \n" .
" Exit code: $exitCode \n" .
" Output:" . implode("\n", $output)
);
}
codecept_debug("[Db] Populator Finished.");
return true;
}
public function getBuiltCommand()
{
return $this->builtCommand;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Codeception\Lib;
use Codeception\Exception\InjectionException;
class Di
{
const DEFAULT_INJECT_METHOD_NAME = '_inject';
protected $container = [];
/**
* @var Di
*/
protected $fallback;
public function __construct($fallback = null)
{
$this->fallback = $fallback;
}
public function get($className)
{
// normalize namespace
$className = ltrim($className, '\\');
return isset($this->container[$className]) ? $this->container[$className] : null;
}
public function set($class)
{
$this->container[get_class($class)] = $class;
}
/**
* @param string $className
* @param array $constructorArgs
* @param string $injectMethodName Method which will be invoked after object creation;
* Resolved dependencies will be passed to it as arguments
* @throws InjectionException
* @return null|object
*/
public function instantiate(
$className,
$constructorArgs = null,
$injectMethodName = self::DEFAULT_INJECT_METHOD_NAME
) {
// normalize namespace
$className = ltrim($className, '\\');
// get class from container
if (isset($this->container[$className])) {
if ($this->container[$className] instanceof $className) {
return $this->container[$className];
}
throw new InjectionException("Failed to resolve cyclic dependencies for class '$className'");
}
// get class from parent container
if ($this->fallback) {
if ($class = $this->fallback->get($className)) {
return $class;
}
}
$this->container[$className] = false; // flag that object is being instantiated
$reflectedClass = new \ReflectionClass($className);
if (!$reflectedClass->isInstantiable()) {
return null;
}
$reflectedConstructor = $reflectedClass->getConstructor();
if (is_null($reflectedConstructor)) {
$object = new $className;
} else {
try {
if (!$constructorArgs) {
$constructorArgs = $this->prepareArgs($reflectedConstructor);
}
} catch (\Exception $e) {
throw new InjectionException("Failed to create instance of '$className'. " . $e->getMessage());
}
$object = $reflectedClass->newInstanceArgs($constructorArgs);
}
if ($injectMethodName) {
$this->injectDependencies($object, $injectMethodName);
}
$this->container[$className] = $object;
return $object;
}
/**
* @param $object
* @param string $injectMethodName Method which will be invoked with resolved dependencies as its arguments
* @throws InjectionException
*/
public function injectDependencies($object, $injectMethodName = self::DEFAULT_INJECT_METHOD_NAME, $defaults = [])
{
if (!is_object($object)) {
return;
}
$reflectedObject = new \ReflectionObject($object);
if (!$reflectedObject->hasMethod($injectMethodName)) {
return;
}
$reflectedMethod = $reflectedObject->getMethod($injectMethodName);
try {
$args = $this->prepareArgs($reflectedMethod, $defaults);
} catch (\Exception $e) {
$msg = $e->getMessage();
if ($e->getPrevious()) { // injection failed because PHP code is invalid. See #3869
$msg .= '; '. $e->getPrevious();
}
throw new InjectionException(
"Failed to inject dependencies in instance of '{$reflectedObject->name}'. $msg"
);
}
if (!$reflectedMethod->isPublic()) {
$reflectedMethod->setAccessible(true);
}
$reflectedMethod->invokeArgs($object, $args);
}
/**
* @param \ReflectionMethod $method
* @param $defaults
* @throws InjectionException
* @return array
*/
protected function prepareArgs(\ReflectionMethod $method, $defaults = [])
{
$args = [];
$parameters = $method->getParameters();
foreach ($parameters as $k => $parameter) {
$dependency = $parameter->getClass();
if (is_null($dependency)) {
if (!$parameter->isOptional()) {
if (!isset($defaults[$k])) {
throw new InjectionException("Parameter '$parameter->name' must have default value.");
}
$args[] = $defaults[$k];
continue;
}
$args[] = $parameter->getDefaultValue();
} else {
$arg = $this->instantiate($dependency->name);
if (is_null($arg)) {
throw new InjectionException("Failed to resolve dependency '{$dependency->name}'.");
}
$args[] = $arg;
}
}
return $args;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Exception\TestRuntimeException;
use Codeception\Lib\Interfaces\Queue;
use Aws\Sqs\SqsClient;
use Aws\Credentials\Credentials;
class AmazonSQS implements Queue
{
protected $queue;
/**
* Connect to the queueing server. (AWS, Iron.io and Beanstalkd)
* @param array $config
* @return
*/
public function openConnection($config)
{
$params = [
'region' => $config['region'],
];
if (! empty($config['key']) && ! empty($config['secret'])) {
$params['credentials'] = new Credentials($config['key'], $config['secret']);
}
if (! empty($config['profile'])) {
$params['profile'] = $config['profile'];
}
if (! empty($config['version'])) {
$params['version'] = $config['version'];
}
if (! empty($config['endpoint'])) {
$params['endpoint'] = $config['endpoint'];
}
$this->queue = new SqsClient($params);
if (!$this->queue) {
throw new TestRuntimeException('connection failed or timed-out.');
}
}
/**
* Post/Put a message on to the queue server
*
* @param string $message Message Body to be send
* @param string $queue Queue Name
*/
public function addMessageToQueue($message, $queue)
{
$this->queue->sendMessage([
'QueueUrl' => $this->getQueueURL($queue),
'MessageBody' => $message,
]);
}
/**
* Return a list of queues/tubes on the queueing server
*
* @return array Array of Queues
*/
public function getQueues()
{
$queueNames = [];
$queues = $this->queue->listQueues(['QueueNamePrefix' => ''])->get('QueueUrls');
foreach ($queues as $queue) {
$tokens = explode('/', $queue);
$queueNames[] = $tokens[sizeof($tokens) - 1];
}
return $queueNames;
}
/**
* Count the current number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesCurrentCountOnQueue($queue)
{
return $this->queue->getQueueAttributes([
'QueueUrl' => $this->getQueueURL($queue),
'AttributeNames' => ['ApproximateNumberOfMessages'],
])->get('Attributes')['ApproximateNumberOfMessages'];
}
/**
* Count the total number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesTotalCountOnQueue($queue)
{
return $this->queue->getQueueAttributes([
'QueueUrl' => $this->getQueueURL($queue),
'AttributeNames' => ['ApproximateNumberOfMessages'],
])->get('Attributes')['ApproximateNumberOfMessages'];
}
public function clearQueue($queue)
{
$queueURL = $this->getQueueURL($queue);
while (true) {
$res = $this->queue->receiveMessage(['QueueUrl' => $queueURL]);
if (!$res->getPath('Messages')) {
return;
}
foreach ($res->getPath('Messages') as $msg) {
$this->queue->deleteMessage([
'QueueUrl' => $queueURL,
'ReceiptHandle' => $msg['ReceiptHandle']
]);
}
}
}
/**
* Get the queue/tube URL from the queue name (AWS function only)
*
* @param $queue Queue Name
*
* @return string Queue URL
*/
private function getQueueURL($queue)
{
$queues = $this->queue->listQueues(['QueueNamePrefix' => ''])->get('QueueUrls');
foreach ($queues as $queueURL) {
$tokens = explode('/', $queueURL);
if (strtolower($queue) == strtolower($tokens[sizeof($tokens) - 1])) {
return $queueURL;
}
}
throw new TestRuntimeException('queue [' . $queue . '] not found');
}
public function getRequiredConfig()
{
return ['region'];
}
public function getDefaultConfig()
{
return [];
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Lib\Interfaces\Queue;
use Pheanstalk\Pheanstalk;
use Pheanstalk\Exception\ConnectionException;
class Beanstalk implements Queue
{
/**
* @var Pheanstalk
*/
protected $queue;
public function openConnection($config)
{
$this->queue = new Pheanstalk($config['host'], $config['port'], $config['timeout']);
}
/**
* Post/Put a message on to the queue server
*
* @param string $message Message Body to be send
* @param string $queue Queue Name
*/
public function addMessageToQueue($message, $queue)
{
$this->queue->putInTube($queue, $message);
}
/**
* Count the total number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesTotalCountOnQueue($queue)
{
try {
return $this->queue->statsTube($queue)['total-jobs'];
} catch (ConnectionException $ex) {
\PHPUnit\Framework\Assert::fail("queue [$queue] not found");
}
}
public function clearQueue($queue = 'default')
{
while ($job = $this->queue->reserveFromTube($queue, 0)) {
$this->queue->delete($job);
}
}
/**
* Return a list of queues/tubes on the queueing server
*
* @return array Array of Queues
*/
public function getQueues()
{
return $this->queue->listTubes();
}
/**
* Count the current number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesCurrentCountOnQueue($queue)
{
try {
return $this->queue->statsTube($queue)['current-jobs-ready'];
} catch (ConnectionException $e) {
\PHPUnit\Framework\Assert::fail("queue [$queue] not found");
}
}
public function getRequiredConfig()
{
return ['host'];
}
public function getDefaultConfig()
{
return ['port' => 11300, 'timeout' => 90];
}
}

View File

@@ -0,0 +1,381 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Exception\ModuleException;
class Db
{
/**
* @var \PDO
*/
protected $dbh;
/**
* @var string
*/
protected $dsn;
protected $user;
protected $password;
/**
* @var array
*
* @see http://php.net/manual/de/pdo.construct.php
*/
protected $options;
/**
* associative array with table name => primary-key
*
* @var array
*/
protected $primaryKeys = [];
public static function connect($dsn, $user, $password, $options = null)
{
$dbh = new \PDO($dsn, $user, $password, $options);
$dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $dbh;
}
/**
* @static
*
* @param $dsn
* @param $user
* @param $password
* @param [optional] $options
*
* @see http://php.net/manual/en/pdo.construct.php
* @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants
*
* @return Db|SqlSrv|MySql|Oci|PostgreSql|Sqlite
*/
public static function create($dsn, $user, $password, $options = null)
{
$provider = self::getProvider($dsn);
switch ($provider) {
case 'sqlite':
return new Sqlite($dsn, $user, $password, $options);
case 'mysql':
return new MySql($dsn, $user, $password, $options);
case 'pgsql':
return new PostgreSql($dsn, $user, $password, $options);
case 'mssql':
case 'dblib':
case 'sqlsrv':
return new SqlSrv($dsn, $user, $password, $options);
case 'oci':
return new Oci($dsn, $user, $password, $options);
default:
return new Db($dsn, $user, $password, $options);
}
}
public static function getProvider($dsn)
{
return substr($dsn, 0, strpos($dsn, ':'));
}
/**
* @param $dsn
* @param $user
* @param $password
* @param [optional] $options
*
* @see http://php.net/manual/en/pdo.construct.php
* @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants
*/
public function __construct($dsn, $user, $password, $options = null)
{
$this->dbh = new \PDO($dsn, $user, $password, $options);
$this->dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->dsn = $dsn;
$this->user = $user;
$this->password = $password;
$this->options = $options;
}
public function __destruct()
{
if ($this->dbh->inTransaction()) {
$this->dbh->rollBack();
}
$this->dbh = null;
}
public function getDbh()
{
return $this->dbh;
}
public function getDb()
{
$matches = [];
$matched = preg_match('~dbname=(\w+)~s', $this->dsn, $matches);
if (!$matched) {
return false;
}
return $matches[1];
}
public function cleanup()
{
}
/**
* Set the lock waiting interval for the database session
* @param int $seconds
* @return void
*/
public function setWaitLock($seconds)
{
}
public function load($sql)
{
$query = '';
$delimiter = ';';
$delimiterLength = 1;
foreach ($sql as $sqlLine) {
if (preg_match('/DELIMITER ([\;\$\|\\\\]+)/i', $sqlLine, $match)) {
$delimiter = $match[1];
$delimiterLength = strlen($delimiter);
continue;
}
$parsed = $this->sqlLine($sqlLine);
if ($parsed) {
continue;
}
$query .= "\n" . rtrim($sqlLine);
if (substr($query, -1 * $delimiterLength, $delimiterLength) == $delimiter) {
$this->sqlQuery(substr($query, 0, -1 * $delimiterLength));
$query = '';
}
}
if ($query !== '') {
$this->sqlQuery($query);
}
}
public function insert($tableName, array &$data)
{
$columns = array_map(
[$this, 'getQuotedName'],
array_keys($data)
);
return sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
$this->getQuotedName($tableName),
implode(', ', $columns),
implode(', ', array_fill(0, count($data), '?'))
);
}
public function select($column, $table, array &$criteria)
{
$where = $this->generateWhereClause($criteria);
$query = "SELECT %s FROM %s %s";
return sprintf($query, $column, $this->getQuotedName($table), $where);
}
private function getSupportedOperators()
{
return [
'like',
'!=',
'<=',
'>=',
'<',
'>',
];
}
protected function generateWhereClause(array &$criteria)
{
if (empty($criteria)) {
return '';
}
$operands = $this->getSupportedOperators();
$params = [];
foreach ($criteria as $k => $v) {
if ($v === null) {
$params[] = $this->getQuotedName($k) . " IS NULL ";
unset($criteria[$k]);
continue;
}
$hasOperand = false; // search for equals - no additional operand given
foreach ($operands as $operand) {
if (!stripos($k, " $operand") > 0) {
continue;
}
$hasOperand = true;
$k = str_ireplace(" $operand", '', $k);
$operand = strtoupper($operand);
$params[] = $this->getQuotedName($k) . " $operand ? ";
break;
}
if (!$hasOperand) {
$params[] = $this->getQuotedName($k) . " = ? ";
}
}
return 'WHERE ' . implode('AND ', $params);
}
/**
* @deprecated use deleteQueryByCriteria instead
*/
public function deleteQuery($table, $id, $primaryKey = 'id')
{
$query = 'DELETE FROM ' . $this->getQuotedName($table) . ' WHERE ' . $this->getQuotedName($primaryKey) . ' = ?';
$this->executeQuery($query, [$id]);
}
public function deleteQueryByCriteria($table, array $criteria)
{
$where = $this->generateWhereClause($criteria);
$query = 'DELETE FROM ' . $this->getQuotedName($table) . ' ' . $where;
$this->executeQuery($query, array_values($criteria));
}
public function lastInsertId($table)
{
return $this->getDbh()->lastInsertId();
}
public function getQuotedName($name)
{
return '"' . str_replace('.', '"."', $name) . '"';
}
protected function sqlLine($sql)
{
$sql = trim($sql);
return (
$sql === ''
|| $sql === ';'
|| preg_match('~^((--.*?)|(#))~s', $sql)
);
}
protected function sqlQuery($query)
{
try {
$this->dbh->exec($query);
} catch (\PDOException $e) {
throw new ModuleException(
'Codeception\Module\Db',
$e->getMessage() . "\nSQL query being executed: " . $query
);
}
}
public function executeQuery($query, array $params)
{
$sth = $this->dbh->prepare($query);
if (!$sth) {
throw new \Exception("Query '$query' can't be prepared.");
}
$i = 0;
foreach ($params as $value) {
$i++;
if (is_bool($value)) {
$type = \PDO::PARAM_BOOL;
} elseif (is_int($value)) {
$type = \PDO::PARAM_INT;
} else {
$type = \PDO::PARAM_STR;
}
$sth->bindValue($i, $value, $type);
}
$sth->execute();
return $sth;
}
/**
* @param string $tableName
*
* @return string
* @throws \Exception
* @deprecated use getPrimaryKey instead
*/
public function getPrimaryColumn($tableName)
{
$primaryKey = $this->getPrimaryKey($tableName);
if (empty($primaryKey)) {
return null;
} elseif (count($primaryKey) > 1) {
throw new \Exception(
'getPrimaryColumn method does not support composite primary keys, use getPrimaryKey instead'
);
}
return $primaryKey[0];
}
/**
* @param string $tableName
*
* @return array[string]
*/
public function getPrimaryKey($tableName)
{
return [];
}
/**
* @return bool
*/
protected function flushPrimaryColumnCache()
{
$this->primaryKeys = [];
return empty($this->primaryKeys);
}
public function update($table, array $data, array $criteria)
{
if (empty($data)) {
throw new \InvalidArgumentException(
"Query update can't be prepared without data."
);
}
$set = [];
foreach ($data as $column => $value) {
$set[] = $this->getQuotedName($column) . " = ?";
}
$where = $this->generateWhereClause($criteria);
return sprintf('UPDATE %s SET %s %s', $this->getQuotedName($table), implode(', ', $set), $where);
}
public function getOptions()
{
return $this->options;
}
}

View File

@@ -0,0 +1,176 @@
<?php
/**
* @author tiger
*/
namespace Codeception\Lib\Driver;
use Facebook\Facebook as FacebookSDK;
class Facebook
{
/**
* @var callable
*/
protected $logCallback;
/**
* @var FacebookSDK
*/
protected $fb;
/**
* @var string
*/
protected $appId;
/**
* @var string
*/
protected $appSecret;
/**
* @var string
*/
protected $appToken;
/**
* Facebook constructor.
*
* @param array $config
* @param callable|null $logCallback
*/
public function __construct($config, $logCallback = null)
{
if (is_callable($logCallback)) {
$this->logCallback = $logCallback;
}
$this->fb = new FacebookSDK(
[
'app_id' => $config['app_id'],
'app_secret' => $config['secret'],
'default_graph_version' => 'v2.5', //TODO add to config
]
);
$this->appId = $config['app_id'];
$this->appSecret = $config['secret'];
$this->appToken = $this->appId . '|' . $this->appSecret;
}
/**
* @param string $name
* @param array $permissions
*
* @return array
*/
public function createTestUser($name, array $permissions)
{
$response = $this->executeFacebookRequest(
'POST',
$this->appId . '/accounts/test-users',
[
'name' => $name,
'installed' => true,
'permissions' => $permissions
]
);
return $response->getDecodedBody();
}
public function deleteTestUser($testUserID)
{
$this->executeFacebookRequest('DELETE', '/' . $testUserID);
}
public function getTestUserInfo($testUserAccessToken)
{
$response = $this->executeFacebookRequest(
'GET',
'/me',
$parameters = [],
$testUserAccessToken
);
return $response->getDecodedBody();
}
public function getLastPostsForTestUser($testUserAccessToken)
{
$response = $this->executeFacebookRequest(
'GET',
'/me/feed',
$parameters = [],
$testUserAccessToken
);
return $response->getDecodedBody();
}
public function getVisitedPlaceTagForTestUser($placeId, $testUserAccessToken)
{
$response = $this->executeFacebookRequest(
'GET',
"/$placeId",
$parameters = [],
$testUserAccessToken
);
return $response->getDecodedBody();
}
public function sendPostToFacebook($testUserAccessToken, array $parameters)
{
$response = $this->executeFacebookRequest(
'POST',
'/me/feed',
$parameters,
$testUserAccessToken
);
return $response->getDecodedBody();
}
/**
* @param string $method
* @param string $endpoint
* @param array $parameters
* @param string $token
*
* @return \Facebook\FacebookResponse
*/
private function executeFacebookRequest($method, $endpoint, array $parameters = [], $token = null)
{
if (is_callable($this->logCallback)) {
//used only for debugging:
call_user_func($this->logCallback, 'Facebook API request', func_get_args());
}
if (!$token) {
$token = $this->appToken;
}
switch ($method) {
case 'GET':
$response = $this->fb->get($endpoint, $token);
break;
case 'POST':
$response = $this->fb->post($endpoint, $parameters, $token);
break;
case 'DELETE':
$response = $this->fb->delete($endpoint, $parameters, $token);
break;
default:
throw new \Exception("Facebook driver exception, please add support for method: " . $method);
break;
}
if (is_callable($this->logCallback)) {
call_user_func($this->logCallback, 'Facebook API response', $response->getDecodedBody());
}
return $response;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Lib\Interfaces\Queue;
class Iron implements Queue
{
/**
* @var \IronMQ
*/
protected $queue;
/**
* Connect to the queueing server. (AWS, Iron.io and Beanstalkd)
* @param array $config
* @return
*/
public function openConnection($config)
{
$this->queue = new \IronMQ([
"token" => $config['token'],
"project_id" => $config['project'],
"host" => $config['host']
]);
if (!$this->queue) {
\PHPUnit\Framework\Assert::fail('connection failed or timed-out.');
}
}
/**
* Post/Put a message on to the queue server
*
* @param string $message Message Body to be send
* @param string $queue Queue Name
*/
public function addMessageToQueue($message, $queue)
{
$this->queue->postMessage($queue, $message);
}
/**
* Return a list of queues/tubes on the queueing server
*
* @return array Array of Queues
*/
public function getQueues()
{
// Format the output to suit
$queues = [];
foreach ($this->queue->getQueues() as $queue) {
$queues[] = $queue->name;
}
return $queues;
}
/**
* Count the current number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesCurrentCountOnQueue($queue)
{
try {
return $this->queue->getQueue($queue)->size;
} catch (\Http_Exception $ex) {
\PHPUnit\Framework\Assert::fail("queue [$queue] not found");
}
}
/**
* Count the total number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesTotalCountOnQueue($queue)
{
try {
return $this->queue->getQueue($queue)->total_messages;
} catch (\Http_Exception $e) {
\PHPUnit\Framework\Assert::fail("queue [$queue] not found");
}
}
public function clearQueue($queue)
{
try {
$this->queue->clearQueue($queue);
} catch (\Http_Exception $ex) {
\PHPUnit\Framework\Assert::fail("queue [$queue] not found");
}
}
public function getRequiredConfig()
{
return ['host', 'token', 'project'];
}
public function getDefaultConfig()
{
return [];
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleException;
use MongoDB\Database;
class MongoDb
{
const DEFAULT_PORT = 27017;
private $legacy;
private $dbh;
private $dsn;
private $dbName;
private $host;
private $user;
private $password;
private $client;
private $quiet = '';
public static function connect($dsn, $user, $password)
{
throw new \Exception(__CLASS__ . '::connect() - hm, it looked like this method had become obsolete...');
}
/**
* Connect to the Mongo server using the MongoDB extension.
*/
protected function setupMongoDB($dsn, $options)
{
try {
$this->client = new \MongoDB\Client($dsn, $options);
$this->dbh = $this->client->selectDatabase($this->dbName);
} catch (\MongoDB\Driver\Exception $e) {
throw new ModuleException($this, sprintf('Failed to open Mongo connection: %s', $e->getMessage()));
}
}
/**
* Connect to the Mongo server using the legacy mongo extension.
*/
protected function setupMongo($dsn, $options)
{
try {
$this->client = new \MongoClient($dsn, $options);
$this->dbh = $this->client->selectDB($this->dbName);
} catch (\MongoConnectionException $e) {
throw new ModuleException($this, sprintf('Failed to open Mongo connection: %s', $e->getMessage()));
}
}
/**
* Clean up the Mongo database using the MongoDB extension.
*/
protected function cleanupMongoDB()
{
try {
$this->dbh->drop();
} catch (\MongoDB\Driver\Exception $e) {
throw new \Exception(sprintf('Failed to drop the DB: %s', $e->getMessage()));
}
}
/**
* Clean up the Mongo database using the legacy Mongo extension.
*/
protected function cleanupMongo()
{
try {
$list = $this->dbh->listCollections();
} catch (\MongoException $e) {
throw new \Exception(sprintf('Failed to list collections of the DB: %s', $e->getMessage()));
}
foreach ($list as $collection) {
try {
$collection->drop();
} catch (\MongoException $e) {
throw new \Exception(sprintf('Failed to drop collection: %s', $e->getMessage()));
}
}
}
/**
* $dsn has to contain db_name after the host. E.g. "mongodb://localhost:27017/mongo_test_db"
*
* @static
*
* @param $dsn
* @param $user
* @param $password
*
* @throws ModuleConfigException
* @throws \Exception
*/
public function __construct($dsn, $user, $password)
{
$this->legacy = extension_loaded('mongodb') === false &&
class_exists('\\MongoClient') &&
strpos(\MongoClient::VERSION, 'mongofill') === false;
/* defining DB name */
$this->dbName = preg_replace('/\?.*/', '', substr($dsn, strrpos($dsn, '/') + 1));
if (strlen($this->dbName) == 0) {
throw new ModuleConfigException($this, 'Please specify valid $dsn with DB name after the host:port');
}
/* defining host */
if (strpos($dsn, 'mongodb://') !== false) {
$this->host = str_replace('mongodb://', '', preg_replace('/\?.*/', '', $dsn));
} else {
$this->host = $dsn;
}
$this->host = rtrim(str_replace($this->dbName, '', $this->host), '/');
$options = [
'connect' => true
];
if ($user && $password) {
$options += [
'username' => $user,
'password' => $password
];
}
$this->{$this->legacy ? 'setupMongo' : 'setupMongoDB'}($dsn, $options);
$this->dsn = $dsn;
$this->user = $user;
$this->password = $password;
}
/**
* @static
*
* @param $dsn
* @param $user
* @param $password
*
* @return MongoDb
*/
public static function create($dsn, $user, $password)
{
return new MongoDb($dsn, $user, $password);
}
public function cleanup()
{
$this->{$this->legacy ? 'cleanupMongo' : 'cleanupMongoDB'}();
}
/**
* dump file has to be a javascript document where one can use all the mongo shell's commands
* just FYI: this file can be easily created be RockMongo's export button
*
* @param $dumpFile
*/
public function load($dumpFile)
{
$cmd = sprintf(
'mongo %s %s%s',
$this->host . '/' . $this->dbName,
$this->createUserPasswordCmdString(),
escapeshellarg($dumpFile)
);
shell_exec($cmd);
}
public function loadFromMongoDump($dumpFile)
{
list($host, $port) = $this->getHostPort();
$cmd = sprintf(
"mongorestore %s --host %s --port %s -d %s %s %s",
$this->quiet,
$host,
$port,
$this->dbName,
$this->createUserPasswordCmdString(),
escapeshellarg($dumpFile)
);
shell_exec($cmd);
}
public function loadFromTarGzMongoDump($dumpFile)
{
list($host, $port) = $this->getHostPort();
$getDirCmd = sprintf(
"tar -tf %s | awk 'BEGIN { FS = \"/\" } ; { print $1 }' | uniq",
escapeshellarg($dumpFile)
);
$dirCountCmd = $getDirCmd . ' | wc -l';
if (trim(shell_exec($dirCountCmd)) !== '1') {
throw new ModuleException(
$this,
'Archive MUST contain single directory with db dump'
);
}
$dirName = trim(shell_exec($getDirCmd));
$cmd = sprintf(
'tar -xzf %s && mongorestore %s --host %s --port %s -d %s %s %s && rm -r %s',
escapeshellarg($dumpFile),
$this->quiet,
$host,
$port,
$this->dbName,
$this->createUserPasswordCmdString(),
$dirName,
$dirName
);
shell_exec($cmd);
}
private function createUserPasswordCmdString()
{
if ($this->user && $this->password) {
return sprintf(
'--username %s --password %s ',
$this->user,
$this->password
);
}
return '';
}
public function getDbh()
{
return $this->dbh;
}
public function setDatabase($dbName)
{
$this->dbh = $this->client->{$this->legacy ? 'selectDB' : 'selectDatabase'}($dbName);
}
/**
* Determine if this driver is using the legacy extension or not.
*
* @return bool
*/
public function isLegacy()
{
return $this->legacy;
}
private function getHostPort()
{
$hostPort = explode(':', $this->host);
if (count($hostPort) === 2) {
return $hostPort;
}
if (count($hostPort) === 1) {
return [$hostPort[0], self::DEFAULT_PORT];
}
throw new ModuleException($this, '$dsn MUST be like (mongodb://)<host>:<port>/<db name>');
}
public function setQuiet($quiet)
{
$this->quiet = $quiet ? '--quiet' : '';
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Codeception\Lib\Driver;
class MySql extends Db
{
public function cleanup()
{
$this->dbh->exec('SET FOREIGN_KEY_CHECKS=0;');
$res = $this->dbh->query("SHOW FULL TABLES WHERE TABLE_TYPE LIKE '%TABLE';")->fetchAll();
foreach ($res as $row) {
$this->dbh->exec('drop table `' . $row[0] . '`');
}
$this->dbh->exec('SET FOREIGN_KEY_CHECKS=1;');
}
protected function sqlQuery($query)
{
$this->dbh->exec('SET FOREIGN_KEY_CHECKS=0;');
parent::sqlQuery($query);
$this->dbh->exec('SET FOREIGN_KEY_CHECKS=1;');
}
public function getQuotedName($name)
{
return '`' . str_replace('.', '`.`', $name) . '`';
}
/**
* @param string $tableName
*
* @return array[string]
*/
public function getPrimaryKey($tableName)
{
if (!isset($this->primaryKeys[$tableName])) {
$primaryKey = [];
$stmt = $this->getDbh()->query(
'SHOW KEYS FROM ' . $this->getQuotedName($tableName) . ' WHERE Key_name = "PRIMARY"'
);
$columns = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($columns as $column) {
$primaryKey []= $column['Column_name'];
}
$this->primaryKeys[$tableName] = $primaryKey;
}
return $this->primaryKeys[$tableName];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Codeception\Lib\Driver;
class Oci extends Db
{
public function setWaitLock($seconds)
{
$this->dbh->exec('ALTER SESSION SET ddl_lock_timeout = ' . (int) $seconds);
}
public function cleanup()
{
$this->dbh->exec(
"BEGIN
FOR i IN (SELECT trigger_name FROM user_triggers)
LOOP
EXECUTE IMMEDIATE('DROP TRIGGER ' || user || '.\"' || i.trigger_name || '\"');
END LOOP;
END;"
);
$this->dbh->exec(
"BEGIN
FOR i IN (SELECT table_name FROM user_tables)
LOOP
EXECUTE IMMEDIATE('DROP TABLE ' || user || '.\"' || i.table_name || '\" CASCADE CONSTRAINTS');
END LOOP;
END;"
);
$this->dbh->exec(
"BEGIN
FOR i IN (SELECT sequence_name FROM user_sequences)
LOOP
EXECUTE IMMEDIATE('DROP SEQUENCE ' || user || '.\"' || i.sequence_name || '\"');
END LOOP;
END;"
);
$this->dbh->exec(
"BEGIN
FOR i IN (SELECT view_name FROM user_views)
LOOP
EXECUTE IMMEDIATE('DROP VIEW ' || user || '.\"' || i.view_name || '\"');
END LOOP;
END;"
);
}
/**
* SQL commands should ends with `//` in the dump file
* IF you want to load triggers too.
* IF you do not want to load triggers you can use the `;` characters
* but in this case you need to change the $delimiter from `//` to `;`
*
* @param $sql
*/
public function load($sql)
{
$query = '';
$delimiter = '//';
$delimiterLength = 2;
foreach ($sql as $sqlLine) {
if (preg_match('/DELIMITER ([\;\$\|\\\\]+)/i', $sqlLine, $match)) {
$delimiter = $match[1];
$delimiterLength = strlen($delimiter);
continue;
}
$parsed = $this->sqlLine($sqlLine);
if ($parsed) {
continue;
}
$query .= "\n" . rtrim($sqlLine);
if (substr($query, -1 * $delimiterLength, $delimiterLength) == $delimiter) {
$this->sqlQuery(substr($query, 0, -1 * $delimiterLength));
$query = "";
}
}
if ($query !== '') {
$this->sqlQuery($query);
}
}
/**
* @param string $tableName
*
* @return array[string]
*/
public function getPrimaryKey($tableName)
{
if (!isset($this->primaryKeys[$tableName])) {
$primaryKey = [];
$query = "SELECT cols.column_name
FROM all_constraints cons, all_cons_columns cols
WHERE cols.table_name = ?
AND cons.constraint_type = 'P'
AND cons.constraint_name = cols.constraint_name
AND cons.owner = cols.owner
ORDER BY cols.table_name, cols.position";
$stmt = $this->executeQuery($query, [$tableName]);
$columns = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($columns as $column) {
$primaryKey []= $column['COLUMN_NAME'];
}
$this->primaryKeys[$tableName] = $primaryKey;
}
return $this->primaryKeys[$tableName];
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Exception\ModuleException;
class PostgreSql extends Db
{
protected $putline = false;
protected $connection = null;
protected $searchPath = null;
/**
* Loads a SQL file.
*
* @param string $sql sql file
*/
public function load($sql)
{
$query = '';
$delimiter = ';';
$delimiterLength = 1;
$dollarsOpen = false;
foreach ($sql as $sqlLine) {
if (preg_match('/DELIMITER ([\;\$\|\\\\]+)/i', $sqlLine, $match)) {
$delimiter = $match[1];
$delimiterLength = strlen($delimiter);
continue;
}
$parsed = trim($query) == '' && $this->sqlLine($sqlLine);
if ($parsed) {
continue;
}
// Ignore $$ inside SQL standard string syntax such as in INSERT statements.
if (!preg_match('/\'.*\$\$.*\'/', $sqlLine)) {
$pos = strpos($sqlLine, '$$');
if (($pos !== false) && ($pos >= 0)) {
$dollarsOpen = !$dollarsOpen;
}
}
if (preg_match('/SET search_path = .*/i', $sqlLine, $match)) {
$this->searchPath = $match[0];
}
$query .= "\n" . rtrim($sqlLine);
if (!$dollarsOpen && substr($query, -1 * $delimiterLength, $delimiterLength) == $delimiter) {
$this->sqlQuery(substr($query, 0, -1 * $delimiterLength));
$query = '';
}
}
if ($query !== '') {
$this->sqlQuery($query);
}
}
public function cleanup()
{
$this->dbh->exec('DROP SCHEMA IF EXISTS public CASCADE;');
$this->dbh->exec('CREATE SCHEMA public;');
}
public function sqlLine($sql)
{
if (!$this->putline) {
return parent::sqlLine($sql);
}
if ($sql == '\.') {
$this->putline = false;
pg_put_line($this->connection, $sql . "\n");
pg_end_copy($this->connection);
pg_close($this->connection);
} else {
pg_put_line($this->connection, $sql . "\n");
}
return true;
}
public function sqlQuery($query)
{
if (strpos(trim($query), 'COPY ') === 0) {
if (!extension_loaded('pgsql')) {
throw new ModuleException(
'\Codeception\Module\Db',
"To run 'COPY' commands 'pgsql' extension should be installed"
);
}
if (defined('HHVM_VERSION')) {
throw new ModuleException(
'\Codeception\Module\Db',
"'COPY' command is not supported on HHVM, please use INSERT instead"
);
}
$constring = str_replace(';', ' ', substr($this->dsn, 6));
$constring .= ' user=' . $this->user;
$constring .= ' password=' . $this->password;
$this->connection = pg_connect($constring);
if ($this->searchPath !== null) {
pg_query($this->connection, $this->searchPath);
}
pg_query($this->connection, $query);
$this->putline = true;
} else {
$this->dbh->exec($query);
}
}
/**
* Get the last inserted ID of table.
*/
public function lastInsertId($table)
{
/*
* We make an assumption that the sequence name for this table
* is based on how postgres names sequences for SERIAL columns
*/
$sequenceName = $this->getQuotedName($table . '_id_seq');
$lastSequence = null;
try {
$lastSequence = $this->getDbh()->lastInsertId($sequenceName);
} catch (\PDOException $e) {
// in this case, the sequence name might be combined with the primary key name
}
// here we check if for instance, it's something like table_primary_key_seq instead of table_id_seq
// this could occur when you use some kind of import tool like pgloader
if (!$lastSequence) {
$primaryKeys = $this->getPrimaryKey($table);
$pkName = array_shift($primaryKeys);
$lastSequence = $this->getDbh()->lastInsertId($this->getQuotedName($table . '_' . $pkName . '_seq'));
}
return $lastSequence;
}
/**
* Returns the primary key(s) of the table, based on:
* https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns.
*
* @param string $tableName
*
* @return array[string]
*/
public function getPrimaryKey($tableName)
{
if (!isset($this->primaryKeys[$tableName])) {
$primaryKey = [];
$query = "SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = '$tableName'::regclass
AND i.indisprimary";
$stmt = $this->executeQuery($query, []);
$columns = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($columns as $column) {
$primaryKey []= $column['attname'];
}
$this->primaryKeys[$tableName] = $primaryKey;
}
return $this->primaryKeys[$tableName];
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Codeception\Lib\Driver;
class SqlSrv extends Db
{
public function getDb()
{
$matches = [];
$matched = preg_match('~Database=(.*);?~s', $this->dsn, $matches);
if (!$matched) {
return false;
}
return $matches[1];
}
public function cleanup()
{
$this->dbh->exec(
"
DECLARE constraints_cursor CURSOR FOR SELECT name, parent_object_id FROM sys.foreign_keys;
OPEN constraints_cursor
DECLARE @constraint sysname;
DECLARE @parent int;
DECLARE @table nvarchar(128);
FETCH NEXT FROM constraints_cursor INTO @constraint, @parent;
WHILE (@@FETCH_STATUS <> -1)
BEGIN
SET @table = OBJECT_NAME(@parent)
EXEC ('ALTER TABLE [' + @table + '] DROP CONSTRAINT [' + @constraint + ']')
FETCH NEXT FROM constraints_cursor INTO @constraint, @parent;
END
DEALLOCATE constraints_cursor;"
);
$this->dbh->exec(
"
DECLARE tables_cursor CURSOR FOR SELECT name FROM sysobjects WHERE type = 'U';
OPEN tables_cursor DECLARE @tablename sysname;
FETCH NEXT FROM tables_cursor INTO @tablename;
WHILE (@@FETCH_STATUS <> -1)
BEGIN
EXEC ('DROP TABLE [' + @tablename + ']')
FETCH NEXT FROM tables_cursor INTO @tablename;
END
DEALLOCATE tables_cursor;"
);
}
protected function generateWhereClause(array &$criteria)
{
if (empty($criteria)) {
return '';
}
$params = [];
foreach ($criteria as $k => $v) {
if ($v === null) {
$params[] = $this->getQuotedName($k) . " IS NULL ";
unset($criteria[$k]);
continue;
}
if (strpos(strtolower($k), ' like') > 0) {
$k = str_replace(' like', '', strtolower($k));
$params[] = $this->getQuotedName($k) . " LIKE ? ";
} else {
$params[] = $this->getQuotedName($k) . " = ? ";
}
}
return 'WHERE ' . implode('AND ', $params);
}
public function getQuotedName($name)
{
return '[' . str_replace('.', '].[', $name) . ']';
}
/**
* @param string $tableName
*
* @return array[string]
*/
public function getPrimaryKey($tableName)
{
if (!isset($this->primaryKeys[$tableName])) {
$primaryKey = [];
$query = "
SELECT Col.Column_Name from
INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab,
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col
WHERE
Col.Constraint_Name = Tab.Constraint_Name
AND Col.Table_Name = Tab.Table_Name
AND Constraint_Type = 'PRIMARY KEY' AND Col.Table_Name = ?";
$stmt = $this->executeQuery($query, [$tableName]);
$columns = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($columns as $column) {
$primaryKey []= $column['Column_Name'];
}
$this->primaryKeys[$tableName] = $primaryKey;
}
return $this->primaryKeys[$tableName];
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Codeception\Lib\Driver;
use Codeception\Configuration;
use Codeception\Exception\ModuleException;
class Sqlite extends Db
{
protected $hasSnapshot = false;
protected $filename = '';
protected $con = null;
public function __construct($dsn, $user, $password, $options = null)
{
$filename = substr($dsn, 7);
if ($filename === ':memory:') {
throw new ModuleException(__CLASS__, ':memory: database is not supported');
}
$this->filename = Configuration::projectDir() . $filename;
$this->dsn = 'sqlite:' . $this->filename;
parent::__construct($this->dsn, $user, $password, $options);
}
public function cleanup()
{
$this->dbh = null;
file_put_contents($this->filename, '');
$this->dbh = self::connect($this->dsn, $this->user, $this->password);
}
public function load($sql)
{
if ($this->hasSnapshot) {
$this->dbh = null;
file_put_contents($this->filename, file_get_contents($this->filename . '_snapshot'));
$this->dbh = new \PDO($this->dsn, $this->user, $this->password);
} else {
if (file_exists($this->filename . '_snapshot')) {
unlink($this->filename . '_snapshot');
}
parent::load($sql);
copy($this->filename, $this->filename . '_snapshot');
$this->hasSnapshot = true;
}
}
/**
* @param string $tableName
*
* @return array[string]
*/
public function getPrimaryKey($tableName)
{
if (!isset($this->primaryKeys[$tableName])) {
if ($this->hasRowId($tableName)) {
return $this->primaryKeys[$tableName] = ['_ROWID_'];
}
$primaryKey = [];
$query = 'PRAGMA table_info(' . $this->getQuotedName($tableName) . ')';
$stmt = $this->executeQuery($query, []);
$columns = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($columns as $column) {
if ($column['pk'] !== '0') {
$primaryKey []= $column['name'];
}
}
$this->primaryKeys[$tableName] = $primaryKey;
}
return $this->primaryKeys[$tableName];
}
/**
* @param $tableName
* @return bool
*/
private function hasRowId($tableName)
{
$params = ['type' => 'table', 'name' => $tableName];
$select = $this->select('sql', 'sqlite_master', $params);
$result = $this->executeQuery($select, $params);
$sql = $result->fetchColumn(0);
return strpos($sql, ') WITHOUT ROWID') === false;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Codeception\Lib;
/**
* Abstract module for PHP frameworks connected via Symfony BrowserKit components
* Each framework is connected with it's own connector defined in \Codeception\Lib\Connector
* Each module for framework should extend this class.
*
*/
abstract class Framework extends InnerBrowser
{
/**
* Returns a list of recognized domain names
*
* @return array
*/
protected function getInternalDomains()
{
return [];
}
public function _beforeSuite($settings = [])
{
/**
* reset internal domains before suite, because each suite can have a different configuration
*/
$this->internalDomains = null;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Codeception\Lib;
use Codeception\Actor;
use Codeception\Exception\TestRuntimeException;
class Friend
{
protected $name;
protected $actor;
protected $data = [];
protected $multiSessionModules = [];
public function __construct($name, Actor $actor, $modules = [])
{
$this->name = $name;
$this->actor = $actor;
$this->multiSessionModules = array_filter($modules, function ($m) {
return $m instanceof Interfaces\MultiSession;
});
if (empty($this->multiSessionModules)) {
throw new TestRuntimeException("No multisession modules used. Can't instantiate friend");
}
}
public function does($closure)
{
$currentUserData = [];
foreach ($this->multiSessionModules as $module) {
$name = $module->_getName();
$currentUserData[$name] = $module->_backupSession();
if (empty($this->data[$name])) {
$module->_initializeSession();
$this->data[$name] = $module->_backupSession();
continue;
}
$module->_loadSession($this->data[$name]);
};
$this->actor->comment(strtoupper("{$this->name} does ---"));
$ret = $closure($this->actor);
$this->actor->comment(strtoupper("--- {$this->name} finished"));
foreach ($this->multiSessionModules as $module) {
$name = $module->_getName();
$this->data[$name] = $module->_backupSession();
$module->_loadSession($currentUserData[$name]);
};
return $ret;
}
public function isGoingTo($argumentation)
{
$this->actor->amGoingTo($argumentation);
}
public function expects($prediction)
{
$this->actor->expect($prediction);
}
public function expectsTo($prediction)
{
$this->actor->expectTo($prediction);
}
public function leave()
{
foreach ($this->multiSessionModules as $module) {
if (isset($this->data[$module->_getName()])) {
$module->_closeSession($this->data[$module->_getName()]);
}
}
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Codecept;
use Codeception\Configuration;
use Codeception\Lib\Di;
use Codeception\Lib\ModuleContainer;
use Codeception\Util\Template;
class Actions
{
protected $template = <<<EOF
<?php //[STAMP] {{hash}}
namespace {{namespace}}_generated;
// This class was automatically generated by build task
// You should not change it manually as it will be overwritten on next build
// @codingStandardsIgnoreFile
trait {{name}}Actions
{
/**
* @return \Codeception\Scenario
*/
abstract protected function getScenario();
{{methods}}
}
EOF;
protected $methodTemplate = <<<EOF
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
{{doc}}
* @see \{{module}}::{{method}}()
*/
public function {{action}}({{params}}) {
return \$this->getScenario()->runStep(new \Codeception\Step\{{step}}('{{method}}', func_get_args()));
}
EOF;
protected $name;
protected $settings;
protected $modules = [];
protected $actions;
protected $numMethods = 0;
public function __construct($settings)
{
$this->name = $settings['actor'];
$this->settings = $settings;
$this->di = new Di();
$modules = Configuration::modules($this->settings);
$this->moduleContainer = new ModuleContainer($this->di, $settings);
foreach ($modules as $moduleName) {
$this->moduleContainer->create($moduleName);
}
$this->modules = $this->moduleContainer->all();
$this->actions = $this->moduleContainer->getActions();
}
public function produce()
{
$namespace = rtrim($this->settings['namespace'], '\\');
$methods = [];
$code = [];
foreach ($this->actions as $action => $moduleName) {
if (in_array($action, $methods)) {
continue;
}
$class = new \ReflectionClass($this->modules[$moduleName]);
$method = $class->getMethod($action);
$code[] = $this->addMethod($method);
$methods[] = $action;
$this->numMethods++;
}
return (new Template($this->template))
->place('namespace', $namespace ? $namespace . '\\' : '')
->place('hash', self::genHash($this->modules, $this->settings))
->place('name', $this->name)
->place('methods', implode("\n\n ", $code))
->produce();
}
protected function addMethod(\ReflectionMethod $refMethod)
{
$class = $refMethod->getDeclaringClass();
$params = $this->getParamsString($refMethod);
$module = $class->getName();
$body = '';
$doc = $this->addDoc($class, $refMethod);
$doc = str_replace('/**', '', $doc);
$doc = trim(str_replace('*/', '', $doc));
if (!$doc) {
$doc = "*";
}
$conditionalDoc = $doc . "\n * Conditional Assertion: Test won't be stopped on fail";
$methodTemplate = (new Template($this->methodTemplate))
->place('module', $module)
->place('method', $refMethod->name)
->place('params', $params);
// generate conditional assertions
if (0 === strpos($refMethod->name, 'see')) {
$type = 'Assertion';
$body .= $methodTemplate
->place('doc', $conditionalDoc)
->place('action', 'can' . ucfirst($refMethod->name))
->place('step', 'ConditionalAssertion')
->produce();
// generate negative assertion
} elseif (0 === strpos($refMethod->name, 'dontSee')) {
$type = 'Assertion';
$body .= $methodTemplate
->place('doc', $conditionalDoc)
->place('action', str_replace('dont', 'cant', $refMethod->name))
->place('step', 'ConditionalAssertion')
->produce();
} elseif (0 === strpos($refMethod->name, 'am')) {
$type = 'Condition';
} else {
$type = 'Action';
}
$body .= $methodTemplate
->place('doc', $doc)
->place('action', $refMethod->name)
->place('step', $type)
->produce();
return $body;
}
/**
* @param \ReflectionMethod $refMethod
* @return array
*/
protected function getParamsString(\ReflectionMethod $refMethod)
{
$params = [];
foreach ($refMethod->getParameters() as $param) {
if ($param->isOptional()) {
$params[] = '$' . $param->name . ' = null';
} else {
$params[] = '$' . $param->name;
};
}
return implode(', ', $params);
}
/**
* @param \ReflectionClass $class
* @param \ReflectionMethod $refMethod
* @return string
*/
protected function addDoc(\ReflectionClass $class, \ReflectionMethod $refMethod)
{
$doc = $refMethod->getDocComment();
if (!$doc) {
$interfaces = $class->getInterfaces();
foreach ($interfaces as $interface) {
$i = new \ReflectionClass($interface->name);
if ($i->hasMethod($refMethod->name)) {
$doc = $i->getMethod($refMethod->name)->getDocComment();
break;
}
}
}
if (!$doc and $class->getParentClass()) {
$parent = new \ReflectionClass($class->getParentClass()->name);
if ($parent->hasMethod($refMethod->name)) {
$doc = $parent->getMethod($refMethod->name)->getDocComment();
return $doc;
}
return $doc;
}
return $doc;
}
public static function genHash($modules, $settings)
{
$actions = [];
foreach ($modules as $moduleName => $module) {
$actions[$moduleName] = get_class_methods(get_class($module));
}
return md5(Codecept::VERSION . serialize($actions) . serialize($settings['modules']));
}
public function getNumMethods()
{
return $this->numMethods;
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Configuration;
use Codeception\Lib\Di;
use Codeception\Lib\ModuleContainer;
use Codeception\Util\Template;
class Actor
{
protected $template = <<<EOF
<?php
{{hasNamespace}}
/**
* Inherited Methods
{{inheritedMethods}}
*
* @SuppressWarnings(PHPMD)
*/
class {{actor}} extends \Codeception\Actor
{
use _generated\{{actor}}Actions;
/**
* Define custom actions here
*/
}
EOF;
protected $inheritedMethodTemplate = ' * @method {{return}} {{method}}({{params}})';
protected $settings;
protected $modules;
protected $actions;
public function __construct($settings)
{
$this->settings = $settings;
$this->di = new Di();
$this->moduleContainer = new ModuleContainer($this->di, $settings);
$modules = Configuration::modules($this->settings);
foreach ($modules as $moduleName) {
$this->moduleContainer->create($moduleName);
}
$this->modules = $this->moduleContainer->all();
$this->actions = $this->moduleContainer->getActions();
}
public function produce()
{
$namespace = rtrim($this->settings['namespace'], '\\');
if (!isset($this->settings['actor']) && isset($this->settings['class_name'])) {
$this->settings['actor'] = $this->settings['class_name'];
}
return (new Template($this->template))
->place('hasNamespace', $namespace ? "namespace $namespace;" : '')
->place('actor', $this->settings['actor'])
->place('inheritedMethods', $this->prependAbstractActorDocBlocks())
->produce();
}
protected function prependAbstractActorDocBlocks()
{
$inherited = [];
$class = new \ReflectionClass('\Codeception\\Actor');
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if ($method->name == '__call') {
continue;
} // skipping magic
if ($method->name == '__construct') {
continue;
} // skipping magic
$returnType = 'void';
if ($method->name == 'haveFriend') {
$returnType = '\Codeception\Lib\Friend';
}
$params = $this->getParamsString($method);
$inherited[] = (new Template($this->inheritedMethodTemplate))
->place('method', $method->name)
->place('params', $params)
->place('return', $returnType)
->produce();
}
return implode("\n", $inherited);
}
/**
* @param \ReflectionMethod $refMethod
* @return array
*/
protected function getParamsString(\ReflectionMethod $refMethod)
{
$params = [];
foreach ($refMethod->getParameters() as $param) {
if ($param->isOptional()) {
$params[] = '$' . $param->name . ' = '.$this->getDefaultValue($param);
} else {
$params[] = '$' . $param->name;
};
}
return implode(', ', $params);
}
public function getActorName()
{
return $this->settings['actor'];
}
public function getModules()
{
return array_keys($this->modules);
}
/**
* Infer default parameter from the reflection object and format it as PHP (code) string
*
* @param \ReflectionParameter $param
*
* @return string
*/
private function getDefaultValue(\ReflectionParameter $param)
{
if ($param->isDefaultValueAvailable()) {
if (method_exists($param, 'isDefaultValueConstant') && $param->isDefaultValueConstant()) {
$constName = $param->getDefaultValueConstantName();
if (false !== strpos($constName, '::')) {
list($class, $const) = explode('::', $constName);
if (in_array($class, ['self', 'static'])) {
$constName = $param->getDeclaringClass()->getName().'::'.$const;
}
}
return $constName;
}
return $this->phpEncodeValue($param->getDefaultValue());
}
return 'null';
}
/**
* PHP encoded a value
*
* @param mixed $value
*
* @return string
*/
private function phpEncodeValue($value)
{
if (is_array($value)) {
return $this->phpEncodeArray($value);
}
if (is_string($value)) {
return json_encode($value);
}
return var_export($value, true);
}
/**
* Recursively PHP encode an array
*
* @param array $array
*
* @return string
*/
private function phpEncodeArray(array $array)
{
$isPlainArray = function (array $value) {
return ((count($value) === 0)
|| (
(array_keys($value) === range(0, count($value) - 1))
&& (0 === count(array_filter(array_keys($value), 'is_string'))))
);
};
if ($isPlainArray($array)) {
return '['.implode(', ', array_map([$this, 'phpEncodeValue'], $array)).']';
}
return '['.implode(', ', array_map(function ($key) use ($array) {
return $this->phpEncodeValue($key).' => '.$this->phpEncodeValue($array[$key]);
}, array_keys($array))).']';
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Exception\ConfigurationException;
use Codeception\Util\Template;
class Cept
{
protected $template = <<<EOF
<?php {{use}}
\$I = new {{actor}}(\$scenario);
\$I->wantTo('perform actions and see result');
EOF;
protected $settings;
public function __construct($settings)
{
$this->settings = $settings;
}
public function produce()
{
$actor = $this->settings['actor'];
if (!$actor) {
throw new ConfigurationException("Cept can't be created for suite without an actor. Add `actor: SomeTester` to suite config");
}
$use = '';
if (! empty($this->settings['namespace'])) {
$namespace = rtrim($this->settings['namespace'], '\\');
$use = "use {$namespace}\\$actor;";
}
return (new Template($this->template))
->place('actor', $actor)
->place('use', $use)
->produce();
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Exception\ConfigurationException;
use Codeception\Util\Shared\Namespaces;
use Codeception\Util\Template;
class Cest
{
use Shared\Classname;
use Namespaces;
protected $template = <<<EOF
<?php
{{namespace}}
class {{name}}Cest
{
public function _before({{actor}} \$I)
{
}
public function _after({{actor}} \$I)
{
}
// tests
public function tryToTest({{actor}} \$I)
{
}
}
EOF;
protected $settings;
protected $name;
public function __construct($className, $settings)
{
$this->name = $this->removeSuffix($className, 'Cest');
$this->settings = $settings;
}
public function produce()
{
$actor = $this->settings['actor'];
if (!$actor) {
throw new ConfigurationException("Cept can't be created for suite without an actor. Add `actor: SomeTester` to suite config");
}
if (array_key_exists('suite_namespace', $this->settings)) {
$namespace = rtrim($this->settings['suite_namespace'], '\\');
} else {
$namespace = rtrim($this->settings['namespace'], '\\');
}
$ns = $this->getNamespaceHeader($namespace.'\\'.$this->name);
if ($namespace) {
$ns .= "use ".$this->settings['namespace'].'\\'.$actor.";";
}
return (new Template($this->template))
->place('name', $this->getShortClassName($this->name))
->place('namespace', $ns)
->place('actor', $actor)
->produce();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Util\Template;
class Feature
{
protected $template = <<<EOF
Feature: {{name}}
In order to ...
As a ...
I need to ...
Scenario: try {{name}}
EOF;
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function produce()
{
return (new Template($this->template))
->place('name', $this->name)
->produce();
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Codeception\Lib\Generator;
use Behat\Gherkin\Node\StepNode;
use Codeception\Test\Loader\Gherkin;
use Codeception\Util\Template;
use Symfony\Component\Finder\Finder;
class GherkinSnippets
{
protected $template = <<<EOF
/**
* @{{type}} {{text}}
*/
public function {{methodName}}({{params}})
{
throw new \Codeception\Exception\Incomplete("Step `{{text}}` is not defined");
}
EOF;
protected $snippets = [];
protected $processed = [];
protected $features = [];
public function __construct($settings, $test = null)
{
$loader = new Gherkin($settings);
$pattern = $loader->getPattern();
$path = $settings['path'];
if (!empty($test)) {
$path = $settings['path'].'/'.$test;
if (preg_match($pattern, $test)) {
$path = dirname($path);
$pattern = basename($test);
}
}
$finder = Finder::create()
->files()
->sortByName()
->in($path)
->followLinks()
->name($pattern);
foreach ($finder as $file) {
$pathname = str_replace("//", "/", $file->getPathname());
$loader->loadTests($pathname);
}
$availableSteps = $loader->getSteps();
$allSteps = [];
foreach ($availableSteps as $stepGroup) {
$allSteps = array_merge($allSteps, $stepGroup);
}
foreach ($loader->getTests() as $test) {
/** @var $test \Codeception\Test\Gherkin **/
$steps = $test->getScenarioNode()->getSteps();
if ($test->getFeatureNode()->hasBackground()) {
$steps = array_merge($steps, $test->getFeatureNode()->getBackground()->getSteps());
}
foreach ($steps as $step) {
$matched = false;
$text = $step->getText();
if (self::stepHasPyStringArgument($step)) {
// pretend it is inline argument
$text .= ' ""';
}
foreach (array_keys($allSteps) as $pattern) {
if (preg_match($pattern, $text)) {
$matched = true;
break;
}
}
if (!$matched) {
$this->addSnippet($step);
$file = str_ireplace($settings['path'], '', $test->getFeatureNode()->getFile());
if (!in_array($file, $this->features)) {
$this->features[] = $file;
}
}
}
}
}
public function addSnippet(StepNode $step)
{
$args = [];
$pattern = $step->getText();
// match numbers (not in quotes)
if (preg_match_all('~([\d\.])(?=([^"]*"[^"]*")*[^"]*$)~', $pattern, $matches)) {
foreach ($matches[1] as $num => $param) {
$num++;
$args[] = '$num' . $num;
$pattern = str_replace($param, ":num$num", $pattern);
}
}
// match quoted string
if (preg_match_all('~"(.*?)"~', $pattern, $matches)) {
foreach ($matches[1] as $num => $param) {
$num++;
$args[] = '$arg' . $num;
$pattern = str_replace('"'.$param.'"', ":arg$num", $pattern);
}
}
// Has multiline argument at the end of step?
if (self::stepHasPyStringArgument($step)) {
$num = count($args) + 1;
$pattern .= " :arg$num";
$args[] = '$arg' . $num;
}
if (in_array($pattern, $this->processed)) {
return;
}
$methodName = preg_replace('~(\s+?|\'|\"|\W)~', '', ucwords(preg_replace('~"(.*?)"|\d+~', '', $step->getText())));
if (empty($methodName)) {
$methodName = 'step_' . substr(sha1($pattern), 0, 9);
}
$this->snippets[] = (new Template($this->template))
->place('type', $step->getKeywordType())
->place('text', $pattern)
->place('methodName', lcfirst($methodName))
->place('params', implode(', ', $args))
->produce();
$this->processed[] = $pattern;
}
public function getSnippets()
{
return $this->snippets;
}
public function getFeatures()
{
return $this->features;
}
public static function stepHasPyStringArgument(StepNode $step)
{
if ($step->hasArguments()) {
$stepArgs = $step->getArguments();
if ($stepArgs[count($stepArgs) - 1]->getNodeType() == "PyString") {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Util\Shared\Namespaces;
use Codeception\Util\Template;
class Group
{
use Namespaces;
use Shared\Classname;
protected $template = <<<EOF
<?php
namespace {{namespace}};
use \Codeception\Event\TestEvent;
/**
* Group class is Codeception Extension which is allowed to handle to all internal events.
* This class itself can be used to listen events for test execution of one particular group.
* It may be especially useful to create fixtures data, prepare server, etc.
*
* INSTALLATION:
*
* To use this group extension, include it to "extensions" option of global Codeception config.
*/
class {{class}} extends \Codeception\Platform\Group
{
public static \$group = '{{groupName}}';
public function _before(TestEvent \$e)
{
}
public function _after(TestEvent \$e)
{
}
}
EOF;
protected $name;
protected $namespace;
protected $settings;
public function __construct($settings, $name)
{
$this->settings = $settings;
$this->name = $name;
$this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Group\\' . $name);
}
public function produce()
{
$ns = $this->getNamespaceString($this->settings['namespace'] . '\\' . $this->name);
return (new Template($this->template))
->place('class', ucfirst($this->name))
->place('name', $this->name)
->place('namespace', $this->namespace)
->place('groupName', strtolower($this->name))
->produce();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Util\Shared\Namespaces;
use Codeception\Util\Template;
class Helper
{
use Namespaces;
protected $template = <<<EOF
<?php
{{namespace}}
// here you can define custom actions
// all public methods declared in helper class will be available in \$I
class {{name}} extends \\Codeception\\Module
{
}
EOF;
protected $namespace;
protected $name;
public function __construct($name, $namespace = '')
{
$this->namespace = $namespace;
$this->name = $name;
}
public function produce()
{
return (new Template($this->template))
->place('namespace', $this->getNamespaceHeader($this->namespace . '\\Helper\\' . $this->name))
->place('name', $this->getShortClassName($this->name))
->produce();
}
public function getHelperName()
{
return rtrim('\\' . $this->namespace, '\\') . '\\Helper\\' . $this->name;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Util\Shared\Namespaces;
use Codeception\Util\Template;
class PageObject
{
use Namespaces;
use Shared\Classname;
protected $template = <<<EOF
<?php
namespace {{namespace}};
class {{class}}
{
// include url of current page
public static \$URL = '';
/**
* Declare UI map for this page here. CSS or XPath allowed.
* public static \$usernameField = '#username';
* public static \$formSubmitButton = "#mainForm input[type=submit]";
*/
/**
* Basic route example for your current URL
* You can append any additional parameter to URL
* and use it in tests like: Page\\Edit::route('/123-post');
*/
public static function route(\$param)
{
return static::\$URL.\$param;
}
{{actions}}
}
EOF;
protected $actionsTemplate = <<<EOF
/**
* @var \\{{actorClass}};
*/
protected \${{actor}};
public function __construct(\\{{actorClass}} \$I)
{
\$this->{{actor}} = \$I;
}
EOF;
protected $actions = '';
protected $settings;
protected $name;
protected $namespace;
public function __construct($settings, $name)
{
$this->settings = $settings;
$this->name = $this->getShortClassName($name);
$this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Page\\' . $name);
}
public function produce()
{
return (new Template($this->template))
->place('namespace', $this->namespace)
->place('actions', $this->produceActions())
->place('class', $this->name)
->produce();
}
protected function produceActions()
{
if (!isset($this->settings['actor'])) {
return ''; // global pageobject
}
$actor = lcfirst($this->settings['actor']);
$actorClass = $this->settings['actor'];
if (!empty($this->settings['namespace'])) {
$actorClass = rtrim($this->settings['namespace'], '\\') . '\\' . $actorClass;
}
return (new Template($this->actionsTemplate))
->place('actorClass', $actorClass)
->place('actor', $actor)
->place('pageObject', $this->name)
->produce();
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Codeception\Lib\Generator\Shared;
trait Classname
{
protected function removeSuffix($classname, $suffix)
{
$classname = preg_replace('~\.php$~', '', $classname);
return preg_replace("~$suffix$~", '', $classname);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Exception\ConfigurationException;
use Codeception\Util\Shared\Namespaces;
use Codeception\Util\Template;
class StepObject
{
use Namespaces;
use Shared\Classname;
protected $template = <<<EOF
<?php
namespace {{namespace}};
class {{name}} extends {{actorClass}}
{
{{actions}}
}
EOF;
protected $actionTemplate = <<<EOF
public function {{action}}()
{
\$I = \$this;
}
EOF;
protected $settings;
protected $name;
protected $actions = '';
public function __construct($settings, $name)
{
$this->settings = $settings;
$this->name = $this->getShortClassName($name);
$this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Step\\' . $name);
}
public function produce()
{
$actor = $this->settings['actor'];
if (!$actor) {
throw new ConfigurationException("Steps can't be created for suite without an actor");
}
$ns = $this->getNamespaceString($this->settings['namespace'] . '\\' . $actor . '\\' . $this->name);
$ns = ltrim($ns, '\\');
$extended = '\\' . ltrim('\\' . $this->settings['namespace'] . '\\' . $actor, '\\');
return (new Template($this->template))
->place('namespace', $this->namespace)
->place('name', $this->name)
->place('actorClass', $extended)
->place('actions', $this->actions)
->produce();
}
public function createAction($action)
{
$this->actions .= (new Template($this->actionTemplate))
->place('action', $action)
->produce();
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Codeception\Lib\Generator;
use Codeception\Configuration;
use Codeception\Util\Shared\Namespaces;
use Codeception\Util\Template;
class Test
{
use Namespaces;
use Shared\Classname;
protected $template = <<<EOF
<?php
{{namespace}}
class {{name}}Test extends \Codeception\Test\Unit
{
{{tester}}
protected function _before()
{
}
protected function _after()
{
}
// tests
public function testSomeFeature()
{
}
}
EOF;
protected $testerTemplate = <<<EOF
/**
* @var \{{actorClass}}
*/
protected \${{actor}};
EOF;
protected $settings;
protected $name;
public function __construct($settings, $name)
{
$this->settings = $settings;
$this->name = $this->removeSuffix($name, 'Test');
}
public function produce()
{
$actor = $this->settings['actor'];
if ($this->settings['namespace']) {
$actor = $this->settings['namespace'] . '\\' . $actor;
}
$ns = $this->getNamespaceHeader($this->settings['namespace'] . '\\' . $this->name);
$tester = '';
if ($this->settings['actor']) {
$tester = (new Template($this->testerTemplate))
->place('actorClass', $actor)
->place('actor', lcfirst(Configuration::config()['actor_suffix']))
->produce();
}
return (new Template($this->template))
->place('namespace', $ns)
->place('name', $this->getShortClassName($this->name))
->place('tester', $tester)
->produce();
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Codeception\Lib;
use Codeception\Configuration;
use Codeception\Test\Interfaces\Reported;
use Codeception\Test\Descriptor;
use Codeception\TestInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
/**
* Loads information for groups from external sources (config, filesystem)
*/
class GroupManager
{
protected $configuredGroups;
protected $testsInGroups = [];
public function __construct(array $groups)
{
$this->configuredGroups = $groups;
$this->loadGroupsByPattern();
$this->loadConfiguredGroupSettings();
}
/**
* proceeds group names with asterisk:
*
* ```
* "tests/_log/g_*" => [
* "tests/_log/group_1",
* "tests/_log/group_2",
* "tests/_log/group_3",
* ]
* ```
*/
protected function loadGroupsByPattern()
{
foreach ($this->configuredGroups as $group => $pattern) {
if (strpos($group, '*') === false) {
continue;
}
$files = Finder::create()->files()
->name(basename($pattern))
->sortByName()
->in(Configuration::projectDir().dirname($pattern));
$i = 1;
foreach ($files as $file) {
/** @var SplFileInfo $file * */
$this->configuredGroups[str_replace('*', $i, $group)] = dirname($pattern).DIRECTORY_SEPARATOR.$file->getRelativePathname();
$i++;
}
unset($this->configuredGroups[$group]);
}
}
protected function loadConfiguredGroupSettings()
{
foreach ($this->configuredGroups as $group => $tests) {
$this->testsInGroups[$group] = [];
if (is_array($tests)) {
foreach ($tests as $test) {
$file = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $test);
$this->testsInGroups[$group][] = Configuration::projectDir() . $file;
}
} elseif (is_file(Configuration::projectDir() . $tests)) {
$handle = @fopen(Configuration::projectDir() . $tests, "r");
if ($handle) {
while (($test = fgets($handle, 4096)) !== false) {
// if the current line is blank then we need to move to the next line
// otherwise the current codeception directory becomes part of the group
// which causes every single test to run
if (trim($test) === '') {
continue;
}
$file = trim(Configuration::projectDir() . $test);
$file = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $file);
$this->testsInGroups[$group][] = $file;
}
fclose($handle);
}
}
}
}
public function groupsForTest(\PHPUnit\Framework\Test $test)
{
$groups = [];
$filename = Descriptor::getTestFileName($test);
if ($test instanceof TestInterface) {
$groups = $test->getMetadata()->getGroups();
}
if ($test instanceof Reported) {
$info = $test->getReportFields();
if (isset($info['class'])) {
$groups = array_merge($groups, \PHPUnit\Util\Test::getGroups($info['class'], $info['name']));
}
$filename = str_replace(['\\\\', '//'], ['\\', '/'], $info['file']);
}
if ($test instanceof \PHPUnit\Framework\TestCase) {
$groups = array_merge($groups, \PHPUnit\Util\Test::getGroups(get_class($test), $test->getName(false)));
}
if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) {
$firstTest = $test->testAt(0);
if ($firstTest != false && $firstTest instanceof TestInterface) {
$groups = array_merge($groups, $firstTest->getMetadata()->getGroups());
$filename = Descriptor::getTestFileName($firstTest);
}
}
foreach ($this->testsInGroups as $group => $tests) {
foreach ($tests as $testPattern) {
if ($filename == $testPattern) {
$groups[] = $group;
}
if (strpos($filename . ':' . $test->getName(false), $testPattern) === 0) {
$groups[] = $group;
}
if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) {
$firstTest = $test->testAt(0);
if ($firstTest != false && $firstTest instanceof TestInterface) {
if (strpos($filename . ':' . $firstTest->getName(false), $testPattern) === 0) {
$groups[] = $group;
}
}
}
}
}
return array_unique($groups);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
<?php
namespace Codeception\Lib\Interfaces;
/**
* Modules for API testing
*/
interface API
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Codeception\Lib\Interfaces;
interface ActiveRecord extends ORM
{
public function haveRecord($model, $attributes = []);
public function seeRecord($model, $attributes = []);
public function dontSeeRecord($model, $attributes = []);
public function grabRecord($model, $attributes = []);
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Codeception\Lib\Interfaces;
interface ConflictsWithModule
{
/**
* Returns class name or interface of module which can conflict with current.
* @return string
*/
public function _conflicts();
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Codeception\Lib\Interfaces;
interface DataMapper extends ORM, DoctrineProvider
{
public function haveInRepository($entity, array $data);
public function seeInRepository($entity, $params = []);
public function dontSeeInRepository($entity, $params = []);
public function grabFromRepository($entity, $field, $params = []);
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Codeception\Lib\Interfaces;
interface Db
{
/**
* Asserts that a row with the given column values exists.
* Provide table name and column values.
*
* ```php
* <?php
* $I->seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']);
* ```
* Fails if no such user found.
*
* Comparison expressions can be used as well:
*
* ```php
* <?php
* $I->seeInDatabase('posts', ['num_comments >=' => '0']);
* $I->seeInDatabase('users', ['email like' => 'miles@davis.com']);
* ```
*
* Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`.
*
*
* @param string $table
* @param array $criteria
*/
public function seeInDatabase($table, $criteria = []);
/**
* Effect is opposite to ->seeInDatabase
*
* Asserts that there is no record with the given column values in a database.
* Provide table name and column values.
*
* ``` php
* <?php
* $I->dontSeeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']);
* ```
* Fails if such user was found.
*
* Comparison expressions can be used as well:
*
* ```php
* <?php
* $I->dontSeeInDatabase('posts', ['num_comments >=' => '0']);
* $I->dontSeeInDatabase('users', ['email like' => 'miles%']);
* ```
*
* Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`.
*
* @param string $table
* @param array $criteria
*/
public function dontSeeInDatabase($table, $criteria = []);
/**
* Fetches a single column value from a database.
* Provide table name, desired column and criteria.
*
* ``` php
* <?php
* $mail = $I->grabFromDatabase('users', 'email', array('name' => 'Davert'));
* ```
* Comparison expressions can be used as well:
*
* ```php
* <?php
* $post = $I->grabFromDatabase('posts', ['num_comments >=' => 100]);
* $user = $I->grabFromDatabase('users', ['email like' => 'miles%']);
* ```
*
* Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`.
*
* @param string $table
* @param string $column
* @param array $criteria
*
* @return mixed
*/
public function grabFromDatabase($table, $column, $criteria = []);
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Codeception\Lib\Interfaces;
interface DependsOnModule
{
/**
* Specifies class or module which is required for current one.
*
* THis method should return array with key as class name and value as error message
* [className => errorMessage
* ]
* @return mixed
*/
public function _depends();
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Codeception\Lib\Interfaces;
interface DoctrineProvider
{
public function _getEntityManager();
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Codeception\Lib\Interfaces;
interface ElementLocator
{
/**
* Locates element using available Codeception locator types:
*
* * XPath
* * CSS
* * Strict Locator
*
* Use it in Helpers or GroupObject or Extension classes:
*
* ```php
* <?php
* $els = $this->getModule('{{MODULE_NAME}}')->_findElements('.items');
* $els = $this->getModule('{{MODULE_NAME}}')->_findElements(['name' => 'username']);
*
* $editLinks = $this->getModule('{{MODULE_NAME}}')->_findElements(['link' => 'Edit']);
* // now you can iterate over $editLinks and check that all them have valid hrefs
* ```
*
* WebDriver module returns `Facebook\WebDriver\Remote\RemoteWebElement` instances
* PhpBrowser and Framework modules return `Symfony\Component\DomCrawler\Crawler` instances
*
* @api
* @param $locator
* @return array of interactive elements
*/
public function _findElements($locator);
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Codeception\Lib\Interfaces;
interface MultiSession
{
public function _initializeSession();
public function _loadSession($session);
public function _backupSession();
public function _closeSession($session = null);
public function _getName();
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Lib\Interfaces;
interface ORM
{
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Codeception\Lib\Interfaces;
interface PageSourceSaver
{
/**
* Saves page source of to a file
*
* ```php
* $this->getModule('{{MODULE_NAME}}')->_savePageSource(codecept_output_dir().'page.html');
* ```
* @api
* @param $filename
*/
public function _savePageSource($filename);
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Codeception\Lib\Interfaces;
/**
* Interface PartedModule
*
* Module implementing this interface can be loaded partly.
* Parts can be defined by marking methods with `@part` annotations.
* Part of modules can be loaded by specifying part (or several parts) in config:
*
* ```
* modules:
* enabled: [MyModule]
* config:
* MyModule:
* part: usefulActions
* ```
*
*
* @package Codeception\Lib\Interfaces
*/
interface PartedModule
{
public function _parts();
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Codeception\Lib\Interfaces;
interface Queue
{
/**
* Connect to the queueing server.
* @param array $config
* @return
*/
public function openConnection($config);
/**
* Post/Put a message on to the queue server
*
* @param string $message Message Body to be send
* @param string $queue Queue Name
*/
public function addMessageToQueue($message, $queue);
/**
* Return a list of queues/tubes on the queueing server
*
* @return array Array of Queues
*/
public function getQueues();
/**
* Count the current number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesCurrentCountOnQueue($queue);
/**
* Count the total number of messages on the queue.
*
* @param $queue Queue Name
*
* @return int Count
*/
public function getMessagesTotalCountOnQueue($queue);
public function clearQueue($queue);
public function getRequiredConfig();
public function getDefaultConfig();
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Codeception\Lib\Interfaces;
interface Remote
{
/**
* Changes the subdomain for the 'url' configuration parameter.
* Does not open a page; use `amOnPage` for that.
*
* ``` php
* <?php
* // If config is: 'http://mysite.com'
* // or config is: 'http://www.mysite.com'
* // or config is: 'http://company.mysite.com'
*
* $I->amOnSubdomain('user');
* $I->amOnPage('/');
* // moves to http://user.mysite.com/
* ?>
* ```
*
* @param $subdomain
*
* @return mixed
*/
public function amOnSubdomain($subdomain);
/**
* Open web page at the given absolute URL and sets its hostname as the base host.
*
* ``` php
* <?php
* $I->amOnUrl('http://codeception.com');
* $I->amOnPage('/quickstart'); // moves to http://codeception.com/quickstart
* ?>
* ```
*/
public function amOnUrl($url);
public function _getUrl();
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Codeception\Lib\Interfaces;
interface RequiresPackage
{
/**
* Returns list of classes and corresponding packages required for this module
*/
public function _requires();
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Codeception\Lib\Interfaces;
interface ScreenshotSaver
{
/**
* Saves screenshot of current page to a file
*
* ```php
* $this->getModule('{{MODULE_NAME}}')->_saveScreenshot(codecept_output_dir().'screenshot_1.png');
* ```
* @api
* @param $filename
*/
public function _saveScreenshot($filename);
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Codeception\Lib\Interfaces;
interface SessionSnapshot
{
/**
* Saves current cookies into named snapshot in order to restore them in other tests
* This is useful to save session state between tests.
* For example, if user needs log in to site for each test this scenario can be executed once
* while other tests can just restore saved cookies.
*
* ``` php
* <?php
* // inside AcceptanceTester class:
*
* public function login()
* {
* // if snapshot exists - skipping login
* if ($I->loadSessionSnapshot('login')) return;
*
* // logging in
* $I->amOnPage('/login');
* $I->fillField('name', 'jon');
* $I->fillField('password', '123345');
* $I->click('Login');
*
* // saving snapshot
* $I->saveSessionSnapshot('login');
* }
* ?>
* ```
*
* @param $name
* @return mixed
*/
public function saveSessionSnapshot($name);
/**
* Loads cookies from a saved snapshot.
* Allows to reuse same session across tests without additional login.
*
* See [saveSessionSnapshot](#saveSessionSnapshot)
*
* @param $name
* @return mixed
*/
public function loadSessionSnapshot($name);
/**
* Deletes session snapshot.
*
* See [saveSessionSnapshot](#saveSessionSnapshot)
*
* @param $name
* @return mixed
*/
public function deleteSessionSnapshot($name);
}

View File

@@ -0,0 +1,983 @@
<?php
namespace Codeception\Lib\Interfaces;
interface Web
{
/**
* Opens the page for the given relative URI.
*
* ``` php
* <?php
* // opens front page
* $I->amOnPage('/');
* // opens /register page
* $I->amOnPage('/register');
* ```
*
* @param string $page
*/
public function amOnPage($page);
/**
* Checks that the current page contains the given string (case insensitive).
*
* You can specify a specific HTML element (via CSS or XPath) as the second
* parameter to only search within that element.
*
* ``` php
* <?php
* $I->see('Logout'); // I can suppose user is logged in
* $I->see('Sign Up', 'h1'); // I can suppose it's a signup page
* $I->see('Sign Up', '//body/h1'); // with XPath
* $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator
* ```
*
* Note that the search is done after stripping all HTML tags from the body,
* so `$I->see('strong')` will return true for strings like:
*
* - `<p>I am Stronger than thou</p>`
* - `<script>document.createElement('strong');</script>`
*
* But will *not* be true for strings like:
*
* - `<strong>Home</strong>`
* - `<div class="strong">Home</strong>`
* - `<!-- strong -->`
*
* For checking the raw source code, use `seeInSource()`.
*
* @param string $text
* @param string $selector optional
*/
public function see($text, $selector = null);
/**
* Checks that the current page doesn't contain the text specified (case insensitive).
* Give a locator as the second parameter to match a specific region.
*
* ```php
* <?php
* $I->dontSee('Login'); // I can suppose user is already logged in
* $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page
* $I->dontSee('Sign Up','//body/h1'); // with XPath
* $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator
* ```
*
* Note that the search is done after stripping all HTML tags from the body,
* so `$I->dontSee('strong')` will fail on strings like:
*
* - `<p>I am Stronger than thou</p>`
* - `<script>document.createElement('strong');</script>`
*
* But will ignore strings like:
*
* - `<strong>Home</strong>`
* - `<div class="strong">Home</strong>`
* - `<!-- strong -->`
*
* For checking the raw source code, use `seeInSource()`.
*
* @param string $text
* @param string $selector optional
*/
public function dontSee($text, $selector = null);
/**
* Checks that the current page contains the given string in its
* raw source code.
*
* ``` php
* <?php
* $I->seeInSource('<h1>Green eggs &amp; ham</h1>');
* ```
*
* @param $raw
*/
public function seeInSource($raw);
/**
* Checks that the current page contains the given string in its
* raw source code.
*
* ```php
* <?php
* $I->dontSeeInSource('<h1>Green eggs &amp; ham</h1>');
* ```
*
* @param $raw
*/
public function dontSeeInSource($raw);
/**
* Submits the given form on the page, with the given form
* values. Pass the form field's values as an array in the second
* parameter.
*
* Although this function can be used as a short-hand version of
* `fillField()`, `selectOption()`, `click()` etc. it has some important
* differences:
*
* * Only field *names* may be used, not CSS/XPath selectors nor field labels
* * If a field is sent to this function that does *not* exist on the page,
* it will silently be added to the HTTP request. This is helpful for testing
* some types of forms, but be aware that you will *not* get an exception
* like you would if you called `fillField()` or `selectOption()` with
* a missing field.
*
* Fields that are not provided will be filled by their values from the page,
* or from any previous calls to `fillField()`, `selectOption()` etc.
* You don't need to click the 'Submit' button afterwards.
* This command itself triggers the request to form's action.
*
* You can optionally specify which button's value to include
* in the request with the last parameter (as an alternative to
* explicitly setting its value in the second parameter), as
* button values are not otherwise included in the request.
*
* Examples:
*
* ``` php
* <?php
* $I->submitForm('#login', [
* 'login' => 'davert',
* 'password' => '123456'
* ]);
* // or
* $I->submitForm('#login', [
* 'login' => 'davert',
* 'password' => '123456'
* ], 'submitButtonName');
*
* ```
*
* For example, given this sample "Sign Up" form:
*
* ``` html
* <form action="/sign_up">
* Login:
* <input type="text" name="user[login]" /><br/>
* Password:
* <input type="password" name="user[password]" /><br/>
* Do you agree to our terms?
* <input type="checkbox" name="user[agree]" /><br/>
* Select pricing plan:
* <select name="plan">
* <option value="1">Free</option>
* <option value="2" selected="selected">Paid</option>
* </select>
* <input type="submit" name="submitButton" value="Submit" />
* </form>
* ```
*
* You could write the following to submit it:
*
* ``` php
* <?php
* $I->submitForm(
* '#userForm',
* [
* 'user' => [
* 'login' => 'Davert',
* 'password' => '123456',
* 'agree' => true
* ]
* ],
* 'submitButton'
* );
* ```
* Note that "2" will be the submitted value for the "plan" field, as it is
* the selected option.
*
* You can also emulate a JavaScript submission by not specifying any
* buttons in the third parameter to submitForm.
*
* ```php
* <?php
* $I->submitForm(
* '#userForm',
* [
* 'user' => [
* 'login' => 'Davert',
* 'password' => '123456',
* 'agree' => true
* ]
* ]
* );
* ```
*
* This function works well when paired with `seeInFormFields()`
* for quickly testing CRUD interfaces and form validation logic.
*
* ``` php
* <?php
* $form = [
* 'field1' => 'value',
* 'field2' => 'another value',
* 'checkbox1' => true,
* // ...
* ];
* $I->submitForm('#my-form', $form, 'submitButton');
* // $I->amOnPage('/path/to/form-page') may be needed
* $I->seeInFormFields('#my-form', $form);
* ```
*
* Parameter values can be set to arrays for multiple input fields
* of the same name, or multi-select combo boxes. For checkboxes,
* you can use either the string value or boolean `true`/`false` which will
* be replaced by the checkbox's value in the DOM.
*
* ``` php
* <?php
* $I->submitForm('#my-form', [
* 'field1' => 'value',
* 'checkbox' => [
* 'value of first checkbox',
* 'value of second checkbox',
* ],
* 'otherCheckboxes' => [
* true,
* false,
* false
* ],
* 'multiselect' => [
* 'first option value',
* 'second option value'
* ]
* ]);
* ```
*
* Mixing string and boolean values for a checkbox's value is not supported
* and may produce unexpected results.
*
* Field names ending in `[]` must be passed without the trailing square
* bracket characters, and must contain an array for its value. This allows
* submitting multiple values with the same name, consider:
*
* ```php
* <?php
* // This will NOT work correctly
* $I->submitForm('#my-form', [
* 'field[]' => 'value',
* 'field[]' => 'another value', // 'field[]' is already a defined key
* ]);
* ```
*
* The solution is to pass an array value:
*
* ```php
* <?php
* // This way both values are submitted
* $I->submitForm('#my-form', [
* 'field' => [
* 'value',
* 'another value',
* ]
* ]);
* ```
*
* @param $selector
* @param $params
* @param $button
*/
public function submitForm($selector, array $params, $button = null);
/**
* Perform a click on a link or a button, given by a locator.
* If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
* For buttons, the "value" attribute, "name" attribute, and inner text are searched.
* For links, the link text is searched.
* For images, the "alt" attribute and inner text of any parent links are searched.
*
* The second parameter is a context (CSS or XPath locator) to narrow the search.
*
* Note that if the locator matches a button of type `submit`, the form will be submitted.
*
* ``` php
* <?php
* // simple link
* $I->click('Logout');
* // button of form
* $I->click('Submit');
* // CSS button
* $I->click('#form input[type=submit]');
* // XPath
* $I->click('//form/*[@type=submit]');
* // link in context
* $I->click('Logout', '#nav');
* // using strict locator
* $I->click(['link' => 'Login']);
* ?>
* ```
*
* @param $link
* @param $context
*/
public function click($link, $context = null);
/**
* Checks that there's a link with the specified text.
* Give a full URL as the second parameter to match links with that exact URL.
*
* ``` php
* <?php
* $I->seeLink('Logout'); // matches <a href="#">Logout</a>
* $I->seeLink('Logout','/logout'); // matches <a href="/logout">Logout</a>
* ?>
* ```
*
* @param string $text
* @param string $url optional
*/
public function seeLink($text, $url = null);
/**
* Checks that the page doesn't contain a link with the given string.
* If the second parameter is given, only links with a matching "href" attribute will be checked.
*
* ``` php
* <?php
* $I->dontSeeLink('Logout'); // I suppose user is not logged in
* $I->dontSeeLink('Checkout now', '/store/cart.php');
* ?>
* ```
*
* @param string $text
* @param string $url optional
*/
public function dontSeeLink($text, $url = null);
/**
* Checks that current URI contains the given string.
*
* ``` php
* <?php
* // to match: /home/dashboard
* $I->seeInCurrentUrl('home');
* // to match: /users/1
* $I->seeInCurrentUrl('/users/');
* ?>
* ```
*
* @param string $uri
*/
public function seeInCurrentUrl($uri);
/**
* Checks that the current URL is equal to the given string.
* Unlike `seeInCurrentUrl`, this only matches the full URL.
*
* ``` php
* <?php
* // to match root url
* $I->seeCurrentUrlEquals('/');
* ?>
* ```
*
* @param string $uri
*/
public function seeCurrentUrlEquals($uri);
/**
* Checks that the current URL matches the given regular expression.
*
* ``` php
* <?php
* // to match root url
* $I->seeCurrentUrlMatches('~$/users/(\d+)~');
* ?>
* ```
*
* @param string $uri
*/
public function seeCurrentUrlMatches($uri);
/**
* Checks that the current URI doesn't contain the given string.
*
* ``` php
* <?php
* $I->dontSeeInCurrentUrl('/users/');
* ?>
* ```
*
* @param string $uri
*/
public function dontSeeInCurrentUrl($uri);
/**
* Checks that the current URL doesn't equal the given string.
* Unlike `dontSeeInCurrentUrl`, this only matches the full URL.
*
* ``` php
* <?php
* // current url is not root
* $I->dontSeeCurrentUrlEquals('/');
* ?>
* ```
*
* @param string $uri
*/
public function dontSeeCurrentUrlEquals($uri);
/**
* Checks that current url doesn't match the given regular expression.
*
* ``` php
* <?php
* // to match root url
* $I->dontSeeCurrentUrlMatches('~$/users/(\d+)~');
* ?>
* ```
*
* @param string $uri
*/
public function dontSeeCurrentUrlMatches($uri);
/**
* Executes the given regular expression against the current URI and returns the first capturing group.
* If no parameters are provided, the full URI is returned.
*
* ``` php
* <?php
* $user_id = $I->grabFromCurrentUrl('~$/user/(\d+)/~');
* $uri = $I->grabFromCurrentUrl();
* ?>
* ```
*
* @param string $uri optional
*
* @return mixed
*/
public function grabFromCurrentUrl($uri = null);
/**
* Checks that the specified checkbox is checked.
*
* ``` php
* <?php
* $I->seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms
* $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form.
* $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]');
* ?>
* ```
*
* @param $checkbox
*/
public function seeCheckboxIsChecked($checkbox);
/**
* Check that the specified checkbox is unchecked.
*
* ``` php
* <?php
* $I->dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms
* $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form.
* ?>
* ```
*
* @param $checkbox
*/
public function dontSeeCheckboxIsChecked($checkbox);
/**
* Checks that the given input field or textarea *equals* (i.e. not just contains) the given value.
* Fields are matched by label text, the "name" attribute, CSS, or XPath.
*
* ``` php
* <?php
* $I->seeInField('Body','Type your comment here');
* $I->seeInField('form textarea[name=body]','Type your comment here');
* $I->seeInField('form input[type=hidden]','hidden_value');
* $I->seeInField('#searchform input','Search');
* $I->seeInField('//form/*[@name=search]','Search');
* $I->seeInField(['name' => 'search'], 'Search');
* ?>
* ```
*
* @param $field
* @param $value
*/
public function seeInField($field, $value);
/**
* Checks that an input field or textarea doesn't contain the given value.
* For fuzzy locators, the field is matched by label text, CSS and XPath.
*
* ``` php
* <?php
* $I->dontSeeInField('Body','Type your comment here');
* $I->dontSeeInField('form textarea[name=body]','Type your comment here');
* $I->dontSeeInField('form input[type=hidden]','hidden_value');
* $I->dontSeeInField('#searchform input','Search');
* $I->dontSeeInField('//form/*[@name=search]','Search');
* $I->dontSeeInField(['name' => 'search'], 'Search');
* ?>
* ```
*
* @param $field
* @param $value
*/
public function dontSeeInField($field, $value);
/**
* Checks if the array of form parameters (name => value) are set on the form matched with the
* passed selector.
*
* ``` php
* <?php
* $I->seeInFormFields('form[name=myform]', [
* 'input1' => 'value',
* 'input2' => 'other value',
* ]);
* ?>
* ```
*
* For multi-select elements, or to check values of multiple elements with the same name, an
* array may be passed:
*
* ``` php
* <?php
* $I->seeInFormFields('.form-class', [
* 'multiselect' => [
* 'value1',
* 'value2',
* ],
* 'checkbox[]' => [
* 'a checked value',
* 'another checked value',
* ],
* ]);
* ?>
* ```
*
* Additionally, checkbox values can be checked with a boolean.
*
* ``` php
* <?php
* $I->seeInFormFields('#form-id', [
* 'checkbox1' => true, // passes if checked
* 'checkbox2' => false, // passes if unchecked
* ]);
* ?>
* ```
*
* Pair this with submitForm for quick testing magic.
*
* ``` php
* <?php
* $form = [
* 'field1' => 'value',
* 'field2' => 'another value',
* 'checkbox1' => true,
* // ...
* ];
* $I->submitForm('//form[@id=my-form]', $form, 'submitButton');
* // $I->amOnPage('/path/to/form-page') may be needed
* $I->seeInFormFields('//form[@id=my-form]', $form);
* ?>
* ```
*
* @param $formSelector
* @param $params
*/
public function seeInFormFields($formSelector, array $params);
/**
* Checks if the array of form parameters (name => value) are not set on the form matched with
* the passed selector.
*
* ``` php
* <?php
* $I->dontSeeInFormFields('form[name=myform]', [
* 'input1' => 'non-existent value',
* 'input2' => 'other non-existent value',
* ]);
* ?>
* ```
*
* To check that an element hasn't been assigned any one of many values, an array can be passed
* as the value:
*
* ``` php
* <?php
* $I->dontSeeInFormFields('.form-class', [
* 'fieldName' => [
* 'This value shouldn\'t be set',
* 'And this value shouldn\'t be set',
* ],
* ]);
* ?>
* ```
*
* Additionally, checkbox values can be checked with a boolean.
*
* ``` php
* <?php
* $I->dontSeeInFormFields('#form-id', [
* 'checkbox1' => true, // fails if checked
* 'checkbox2' => false, // fails if unchecked
* ]);
* ?>
* ```
*
* @param $formSelector
* @param $params
*/
public function dontSeeInFormFields($formSelector, array $params);
/**
* Selects an option in a select tag or in radio button group.
*
* ``` php
* <?php
* $I->selectOption('form select[name=account]', 'Premium');
* $I->selectOption('form input[name=payment]', 'Monthly');
* $I->selectOption('//form/select[@name=account]', 'Monthly');
* ?>
* ```
*
* Provide an array for the second argument to select multiple options:
*
* ``` php
* <?php
* $I->selectOption('Which OS do you use?', array('Windows','Linux'));
* ?>
* ```
*
* Or provide an associative array for the second argument to specifically define which selection method should be used:
*
* ``` php
* <?php
* $I->selectOption('Which OS do you use?', array('text' => 'Windows')); // Only search by text 'Windows'
* $I->selectOption('Which OS do you use?', array('value' => 'windows')); // Only search by value 'windows'
* ?>
* ```
*
* @param $select
* @param $option
*/
public function selectOption($select, $option);
/**
* Ticks a checkbox. For radio buttons, use the `selectOption` method instead.
*
* ``` php
* <?php
* $I->checkOption('#agree');
* ?>
* ```
*
* @param $option
*/
public function checkOption($option);
/**
* Unticks a checkbox.
*
* ``` php
* <?php
* $I->uncheckOption('#notify');
* ?>
* ```
*
* @param $option
*/
public function uncheckOption($option);
/**
* Fills a text field or textarea with the given string.
*
* ``` php
* <?php
* $I->fillField("//input[@type='text']", "Hello World!");
* $I->fillField(['name' => 'email'], 'jon@mail.com');
* ?>
* ```
*
* @param $field
* @param $value
*/
public function fillField($field, $value);
/**
* Attaches a file relative to the Codeception `_data` directory to the given file upload field.
*
* ``` php
* <?php
* // file is stored in 'tests/_data/prices.xls'
* $I->attachFile('input[@type="file"]', 'prices.xls');
* ?>
* ```
*
* @param $field
* @param $filename
*/
public function attachFile($field, $filename);
/**
* Finds and returns the text contents of the given element.
* If a fuzzy locator is used, the element is found using CSS, XPath,
* and by matching the full page source by regular expression.
*
* ``` php
* <?php
* $heading = $I->grabTextFrom('h1');
* $heading = $I->grabTextFrom('descendant-or-self::h1');
* $value = $I->grabTextFrom('~<input value=(.*?)]~sgi'); // match with a regex
* ?>
* ```
*
* @param $cssOrXPathOrRegex
*
* @return mixed
*/
public function grabTextFrom($cssOrXPathOrRegex);
/**
* Finds the value for the given form field.
* If a fuzzy locator is used, the field is found by field name, CSS, and XPath.
*
* ``` php
* <?php
* $name = $I->grabValueFrom('Name');
* $name = $I->grabValueFrom('input[name=username]');
* $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']');
* $name = $I->grabValueFrom(['name' => 'username']);
* ?>
* ```
*
* @param $field
*
* @return mixed
*/
public function grabValueFrom($field);
/**
* Grabs the value of the given attribute value from the given element.
* Fails if element is not found.
*
* ``` php
* <?php
* $I->grabAttributeFrom('#tooltip', 'title');
* ?>
* ```
*
*
* @param $cssOrXpath
* @param $attribute
*
* @return mixed
*/
public function grabAttributeFrom($cssOrXpath, $attribute);
/**
* Grabs either the text content, or attribute values, of nodes
* matched by $cssOrXpath and returns them as an array.
*
* ```html
* <a href="#first">First</a>
* <a href="#second">Second</a>
* <a href="#third">Third</a>
* ```
*
* ```php
* <?php
* // would return ['First', 'Second', 'Third']
* $aLinkText = $I->grabMultiple('a');
*
* // would return ['#first', '#second', '#third']
* $aLinks = $I->grabMultiple('a', 'href');
* ?>
* ```
*
* @param $cssOrXpath
* @param $attribute
* @return string[]
*/
public function grabMultiple($cssOrXpath, $attribute = null);
/**
* Checks that the given element exists on the page and is visible.
* You can also specify expected attributes of this element.
*
* ``` php
* <?php
* $I->seeElement('.error');
* $I->seeElement('//form/input[1]');
* $I->seeElement('input', ['name' => 'login']);
* $I->seeElement('input', ['value' => '123456']);
*
* // strict locator in first arg, attributes in second
* $I->seeElement(['css' => 'form input'], ['name' => 'login']);
* ?>
* ```
*
* @param $selector
* @param array $attributes
* @return
*/
public function seeElement($selector, $attributes = []);
/**
* Checks that the given element is invisible or not present on the page.
* You can also specify expected attributes of this element.
*
* ``` php
* <?php
* $I->dontSeeElement('.error');
* $I->dontSeeElement('//form/input[1]');
* $I->dontSeeElement('input', ['name' => 'login']);
* $I->dontSeeElement('input', ['value' => '123456']);
* ?>
* ```
*
* @param $selector
* @param array $attributes
*/
public function dontSeeElement($selector, $attributes = []);
/**
* Checks that there are a certain number of elements matched by the given locator on the page.
*
* ``` php
* <?php
* $I->seeNumberOfElements('tr', 10);
* $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements
* ?>
* ```
* @param $selector
* @param mixed $expected int or int[]
*/
public function seeNumberOfElements($selector, $expected);
/**
* Checks that the given option is selected.
*
* ``` php
* <?php
* $I->seeOptionIsSelected('#form input[name=payment]', 'Visa');
* ?>
* ```
*
* @param $selector
* @param $optionText
*
* @return mixed
*/
public function seeOptionIsSelected($selector, $optionText);
/**
* Checks that the given option is not selected.
*
* ``` php
* <?php
* $I->dontSeeOptionIsSelected('#form input[name=payment]', 'Visa');
* ?>
* ```
*
* @param $selector
* @param $optionText
*
* @return mixed
*/
public function dontSeeOptionIsSelected($selector, $optionText);
/**
* Checks that the page title contains the given string.
*
* ``` php
* <?php
* $I->seeInTitle('Blog - Post #1');
* ?>
* ```
*
* @param $title
*
* @return mixed
*/
public function seeInTitle($title);
/**
* Checks that the page title does not contain the given string.
*
* @param $title
*
* @return mixed
*/
public function dontSeeInTitle($title);
/**
* Checks that a cookie with the given name is set.
* You can set additional cookie params like `domain`, `path` as array passed in last argument.
*
* ``` php
* <?php
* $I->seeCookie('PHPSESSID');
* ?>
* ```
*
* @param $cookie
* @param array $params
* @return mixed
*/
public function seeCookie($cookie, array $params = []);
/**
* Checks that there isn't a cookie with the given name.
* You can set additional cookie params like `domain`, `path` as array passed in last argument.
*
* @param $cookie
*
* @param array $params
* @return mixed
*/
public function dontSeeCookie($cookie, array $params = []);
/**
* Sets a cookie with the given name and value.
* You can set additional cookie params like `domain`, `path`, `expires`, `secure` in array passed as last argument.
*
* ``` php
* <?php
* $I->setCookie('PHPSESSID', 'el4ukv0kqbvoirg7nkp4dncpk3');
* ?>
* ```
*
* @param $name
* @param $val
* @param array $params
*
* @return mixed
*/
public function setCookie($name, $val, array $params = []);
/**
* Unsets cookie with the given name.
* You can set additional cookie params like `domain`, `path` in array passed as last argument.
*
* @param $cookie
*
* @param array $params
* @return mixed
*/
public function resetCookie($cookie, array $params = []);
/**
* Grabs a cookie value.
* You can set additional cookie params like `domain`, `path` in array passed as last argument.
*
* @param $cookie
*
* @param array $params
* @return mixed
*/
public function grabCookie($cookie, array $params = []);
/**
* Grabs current page source code.
*
* @return string Current page source code.
*/
public function grabPageSource();
}

View File

@@ -0,0 +1,504 @@
<?php
namespace Codeception\Lib;
use Codeception\Configuration;
use Codeception\Exception\ConfigurationException;
use Codeception\Exception\ModuleConflictException;
use Codeception\Exception\ModuleException;
use Codeception\Exception\ModuleRequireException;
use Codeception\Lib\Interfaces\ConflictsWithModule;
use Codeception\Lib\Interfaces\DependsOnModule;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\Util\Annotation;
/**
* Class ModuleContainer
* @package Codeception\Lib
*/
class ModuleContainer
{
/**
* @var string
*/
const MODULE_NAMESPACE = '\\Codeception\\Module\\';
/**
* @var array
*/
private $config;
/**
* @var Di
*/
private $di;
/**
* @var array
*/
private $modules = [];
/**
* @var array
*/
private $active = [];
/**
* @var array
*/
private $actions = [];
/**
* Constructor.
*
* @param Di $di
* @param array $config
*/
public function __construct(Di $di, $config)
{
$this->di = $di;
$this->di->set($this);
$this->config = $config;
}
/**
* Create a module.
*
* @param string $moduleName
* @param bool $active
* @return \Codeception\Module
* @throws \Codeception\Exception\ConfigurationException
* @throws \Codeception\Exception\ModuleException
* @throws \Codeception\Exception\ModuleRequireException
* @throws \Codeception\Exception\InjectionException
*/
public function create($moduleName, $active = true)
{
$this->active[$moduleName] = $active;
$moduleClass = $this->getModuleClass($moduleName);
if (!class_exists($moduleClass)) {
throw new ConfigurationException("Module $moduleName could not be found and loaded");
}
$config = $this->getModuleConfig($moduleName);
if (empty($config) && !$active) {
// For modules that are a dependency of other modules we want to skip the validation of the config.
// This config validation is performed in \Codeception\Module::__construct().
// Explicitly setting $config to null skips this validation.
$config = null;
}
$this->modules[$moduleName] = $module = $this->di->instantiate($moduleClass, [$this, $config], false);
if ($this->moduleHasDependencies($module)) {
$this->injectModuleDependencies($moduleName, $module);
}
// If module is not active its actions should not be included in the actor class
$actions = $active ? $this->getActionsForModule($module, $config) : [];
foreach ($actions as $action) {
$this->actions[$action] = $moduleName;
};
return $module;
}
/**
* Does a module have dependencies?
*
* @param \Codeception\Module $module
* @return bool
*/
private function moduleHasDependencies($module)
{
if (!$module instanceof DependsOnModule) {
return false;
}
return (bool) $module->_depends();
}
/**
* Get the actions of a module.
*
* @param \Codeception\Module $module
* @param array $config
* @return array
*/
private function getActionsForModule($module, $config)
{
$reflectionClass = new \ReflectionClass($module);
// Only public methods can be actions
$methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC);
// Should this module be loaded partially?
$configuredParts = null;
if ($module instanceof PartedModule && isset($config['part'])) {
$configuredParts = is_array($config['part']) ? $config['part'] : [$config['part']];
}
$actions = [];
foreach ($methods as $method) {
if ($this->includeMethodAsAction($module, $method, $configuredParts)) {
$actions[] = $method->name;
}
}
return $actions;
}
/**
* Should a method be included as an action?
*
* @param \Codeception\Module $module
* @param \ReflectionMethod $method
* @param array|null $configuredParts
* @return bool
*/
private function includeMethodAsAction($module, $method, $configuredParts = null)
{
// Filter out excluded actions
if ($module::$excludeActions && in_array($method->name, $module::$excludeActions)) {
return false;
}
// Keep only the $onlyActions if they are specified
if ($module::$onlyActions && !in_array($method->name, $module::$onlyActions)) {
return false;
}
// Do not include inherited actions if the static $includeInheritedActions property is set to false.
// However, if an inherited action is also specified in the static $onlyActions property
// it should be included as an action.
if (!$module::$includeInheritedActions &&
!in_array($method->name, $module::$onlyActions) &&
$method->getDeclaringClass()->getName() != get_class($module)
) {
return false;
}
// Do not include hidden methods, methods with a name starting with an underscore
if (strpos($method->name, '_') === 0) {
return false;
};
// If a part is configured for the module, only include actions from that part
if ($configuredParts) {
$moduleParts = Annotation::forMethod($module, $method->name)->fetchAll('part');
if (!array_uintersect($moduleParts, $configuredParts, 'strcasecmp')) {
return false;
}
}
return true;
}
/**
* Is the module a helper?
*
* @param string $moduleName
* @return bool
*/
private function isHelper($moduleName)
{
return strpos($moduleName, '\\') !== false;
}
/**
* Get the fully qualified class name for a module.
*
* @param string $moduleName
* @return string
*/
private function getModuleClass($moduleName)
{
if ($this->isHelper($moduleName)) {
return $moduleName;
}
return self::MODULE_NAMESPACE . $moduleName;
}
/**
* Is a module instantiated in this ModuleContainer?
*
* @param string $moduleName
* @return bool
*/
public function hasModule($moduleName)
{
return isset($this->modules[$moduleName]);
}
/**
* Get a module from this ModuleContainer.
*
* @param string $moduleName
* @return \Codeception\Module
* @throws \Codeception\Exception\ModuleException
*/
public function getModule($moduleName)
{
if (!$this->hasModule($moduleName)) {
throw new ModuleException(__CLASS__, "Module $moduleName couldn't be connected");
}
return $this->modules[$moduleName];
}
/**
* Get the module for an action.
*
* @param string $action
* @return \Codeception\Module|null This method returns null if there is no module for $action
*/
public function moduleForAction($action)
{
if (!isset($this->actions[$action])) {
return null;
}
return $this->modules[$this->actions[$action]];
}
/**
* Get all actions.
*
* @return array An array with actions as keys and module names as values.
*/
public function getActions()
{
return $this->actions;
}
/**
* Get all modules.
*
* @return array An array with module names as keys and modules as values.
*/
public function all()
{
return $this->modules;
}
/**
* Mock a module in this ModuleContainer.
*
* @param string $moduleName
* @param object $mock
*/
public function mock($moduleName, $mock)
{
$this->modules[$moduleName] = $mock;
}
/**
* Inject the dependencies of a module.
*
* @param string $moduleName
* @param \Codeception\Lib\Interfaces\DependsOnModule $module
* @throws \Codeception\Exception\ModuleException
* @throws \Codeception\Exception\ModuleRequireException
*/
private function injectModuleDependencies($moduleName, DependsOnModule $module)
{
$this->checkForMissingDependencies($moduleName, $module);
if (!method_exists($module, '_inject')) {
throw new ModuleException($module, 'Module requires method _inject to be defined to accept dependencies');
}
$dependencies = array_map(function ($dependency) {
return $this->create($dependency, false);
}, $this->getConfiguredDependencies($moduleName));
call_user_func_array([$module, '_inject'], $dependencies);
}
/**
* Check for missing dependencies.
*
* @param string $moduleName
* @param \Codeception\Lib\Interfaces\DependsOnModule $module
* @throws \Codeception\Exception\ModuleException
* @throws \Codeception\Exception\ModuleRequireException
*/
private function checkForMissingDependencies($moduleName, DependsOnModule $module)
{
$dependencies = $this->getModuleDependencies($module);
$configuredDependenciesCount = count($this->getConfiguredDependencies($moduleName));
if ($configuredDependenciesCount < count($dependencies)) {
$missingDependency = array_keys($dependencies)[$configuredDependenciesCount];
$message = sprintf(
"\nThis module depends on %s\n\n\n%s",
$missingDependency,
$this->getErrorMessageForDependency($module, $missingDependency)
);
throw new ModuleRequireException($moduleName, $message);
}
}
/**
* Get the dependencies of a module.
*
* @param \Codeception\Lib\Interfaces\DependsOnModule $module
* @return array
* @throws \Codeception\Exception\ModuleException
*/
private function getModuleDependencies(DependsOnModule $module)
{
$depends = $module->_depends();
if (!$depends) {
return [];
}
if (!is_array($depends)) {
$message = sprintf("Method _depends of module '%s' must return an array", get_class($module));
throw new ModuleException($module, $message);
}
return $depends;
}
/**
* Get the configured dependencies for a module.
*
* @param string $moduleName
* @return array
*/
private function getConfiguredDependencies($moduleName)
{
$config = $this->getModuleConfig($moduleName);
if (!isset($config['depends'])) {
return [];
}
return is_array($config['depends']) ? $config['depends'] : [$config['depends']];
}
/**
* Get the error message for a module dependency that is missing.
*
* @param \Codeception\Module $module
* @param string $missingDependency
* @return string
*/
private function getErrorMessageForDependency($module, $missingDependency)
{
$depends = $module->_depends();
return $depends[$missingDependency];
}
/**
* Get the configuration for a module.
*
* A module with name $moduleName can be configured at two paths in a configuration file:
* - modules.config.$moduleName
* - modules.enabled.$moduleName
*
* This method checks both locations for configuration. If there is configuration at both locations
* this method merges them, where the configuration at modules.enabled.$moduleName takes precedence
* over modules.config.$moduleName if the same parameters are configured at both locations.
*
* @param string $moduleName
* @return array
*/
private function getModuleConfig($moduleName)
{
$config = isset($this->config['modules']['config'][$moduleName])
? $this->config['modules']['config'][$moduleName]
: [];
if (!isset($this->config['modules']['enabled'])) {
return $config;
}
if (!is_array($this->config['modules']['enabled'])) {
return $config;
}
foreach ($this->config['modules']['enabled'] as $enabledModuleConfig) {
if (!is_array($enabledModuleConfig)) {
continue;
}
$enabledModuleName = key($enabledModuleConfig);
if ($enabledModuleName === $moduleName) {
return Configuration::mergeConfigs(reset($enabledModuleConfig), $config);
}
}
return $config;
}
/**
* Check if there are conflicting modules in this ModuleContainer.
*
* @throws \Codeception\Exception\ModuleConflictException
*/
public function validateConflicts()
{
$canConflict = [];
foreach ($this->modules as $moduleName => $module) {
$parted = $module instanceof PartedModule && $module->_getConfig('part');
if ($this->active[$moduleName] && !$parted) {
$canConflict[] = $module;
}
}
foreach ($canConflict as $module) {
foreach ($canConflict as $otherModule) {
$this->validateConflict($module, $otherModule);
}
}
}
/**
* Check if the modules passed as arguments to this method conflict with each other.
*
* @param \Codeception\Module $module
* @param \Codeception\Module $otherModule
* @throws \Codeception\Exception\ModuleConflictException
*/
private function validateConflict($module, $otherModule)
{
if ($module === $otherModule || !$module instanceof ConflictsWithModule) {
return;
}
$conflicts = $this->normalizeConflictSpecification($module->_conflicts());
if ($otherModule instanceof $conflicts) {
throw new ModuleConflictException($module, $otherModule);
}
}
/**
* Normalize the return value of ConflictsWithModule::_conflicts() to a class name.
* This is necessary because it can return a module name instead of the name of a class or interface.
*
* @param string $conflicts
* @return string
*/
private function normalizeConflictSpecification($conflicts)
{
if (interface_exists($conflicts) || class_exists($conflicts)) {
return $conflicts;
}
if ($this->hasModule($conflicts)) {
return $this->getModule($conflicts);
}
return $conflicts;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Codeception\Lib;
class Notification
{
protected static $messages = [];
public static function warning($message, $location)
{
self::$messages[] = 'WARNING: ' . self::formatMessage($message, $location);
}
public static function deprecate($message, $location = '')
{
self::$messages[] = 'DEPRECATION: ' . self::formatMessage($message, $location);
}
private static function formatMessage($message, $location = '')
{
if ($location) {
return "<bold>$message</bold> <info>$location</info>";
}
return $message;
}
public static function all()
{
$messages = self::$messages;
self::$messages = [];
return $messages;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Codeception\Lib;
use Codeception\Exception\ConfigurationException;
use Symfony\Component\Yaml\Yaml;
class ParamsLoader
{
protected $paramStorage;
protected $paramsFile;
public function load($paramStorage)
{
$this->paramsFile = null;
$this->paramStorage = $paramStorage;
if (is_array($paramStorage)) {
return $this->loadArray();
}
if ($paramStorage === 'env' || $paramStorage === 'environment') {
return $this->loadEnvironmentVars();
}
$this->paramsFile = codecept_absolute_path($paramStorage);
if (!file_exists($this->paramsFile)) {
throw new ConfigurationException("Params file {$this->paramsFile} not found");
}
try {
if (preg_match('~\.yml$~', $paramStorage)) {
return $this->loadYamlFile();
}
if (preg_match('~\.ini$~', $paramStorage)) {
return $this->loadIniFile();
}
if (preg_match('~\.php$~', $paramStorage)) {
return $this->loadPhpFile();
}
if (preg_match('~(\.env(\.|$))~', $paramStorage)) {
return $this->loadDotEnvFile();
}
} catch (\Exception $e) {
throw new ConfigurationException("Failed loading params from $paramStorage\n" . $e->getMessage());
}
throw new ConfigurationException("Params can't be loaded from `$paramStorage`.");
}
public function loadArray()
{
return $this->paramStorage;
}
protected function loadIniFile()
{
return parse_ini_file($this->paramsFile);
}
protected function loadPhpFile()
{
return require $this->paramsFile;
}
protected function loadYamlFile()
{
$params = Yaml::parse(file_get_contents($this->paramsFile));
if (isset($params['parameters'])) { // Symfony style
$params = $params['parameters'];
}
return $params;
}
protected function loadDotEnvFile()
{
if (class_exists('Dotenv\Dotenv')) {
$dotEnv = new \Dotenv\Dotenv(codecept_root_dir(), $this->paramStorage);
$dotEnv->load();
return $_SERVER;
} elseif (class_exists('Symfony\Component\Dotenv\Dotenv')) {
$dotEnv = new \Symfony\Component\Dotenv\Dotenv();
$dotEnv->load(codecept_root_dir($this->paramStorage));
return $_SERVER;
}
throw new ConfigurationException(
"`vlucas/phpdotenv` library is required to parse .env files.\n" .
"Please install it via composer: composer require vlucas/phpdotenv"
);
}
protected function loadEnvironmentVars()
{
return $_SERVER;
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace Codeception\Lib;
use Codeception\Configuration;
use Codeception\Exception\TestParseException;
use Codeception\Scenario;
use Codeception\Step;
use Codeception\Test\Metadata;
class Parser
{
/**
* @var Scenario
*/
protected $scenario;
/**
* @var Metadata
*/
protected $metadata;
protected $code;
public function __construct(Scenario $scenario, Metadata $metadata)
{
$this->scenario = $scenario;
$this->metadata = $metadata;
}
public function prepareToRun($code)
{
$this->parseFeature($code);
$this->parseScenarioOptions($code);
}
public function parseFeature($code)
{
$matches = [];
$code = $this->stripComments($code);
$res = preg_match("~\\\$I->wantTo\\(\s*?['\"](.*?)['\"]\s*?\\);~", $code, $matches);
if ($res) {
$this->scenario->setFeature($matches[1]);
return;
}
$res = preg_match("~\\\$I->wantToTest\\(['\"](.*?)['\"]\\);~", $code, $matches);
if ($res) {
$this->scenario->setFeature("test " . $matches[1]);
return;
}
}
public function parseScenarioOptions($code)
{
$this->metadata->setParamsFromAnnotations($this->matchComments($code));
}
public function parseSteps($code)
{
// parse per line
$friends = [];
$lines = explode("\n", $code);
$isFriend = false;
foreach ($lines as $line) {
// friends
if (preg_match("~\\\$I->haveFriend\((.*?)\);~", $line, $matches)) {
$friends[] = trim($matches[1], '\'"');
}
// friend's section start
if (preg_match("~\\\$(.*?)->does\(~", $line, $matches)) {
$friend = $matches[1];
if (!in_array($friend, $friends)) {
continue;
}
$isFriend = true;
$this->addCommentStep("\n----- $friend does -----");
continue;
}
// actions
if (preg_match("~\\\$I->(.*)\((.*?)\);~", $line, $matches)) {
$this->addStep($matches);
}
// friend's section ends
if ($isFriend && strpos($line, '}') !== false) {
$this->addCommentStep("-------- back to me\n");
$isFriend = false;
}
}
}
protected function addStep($matches)
{
list($m, $action, $params) = $matches;
if (in_array($action, ['wantTo', 'wantToTest'])) {
return;
}
$this->scenario->addStep(new Step\Action($action, explode(',', $params)));
}
protected function addCommentStep($comment)
{
$this->scenario->addStep(new \Codeception\Step\Comment($comment, []));
}
public static function validate($file)
{
$config = Configuration::config();
if (empty($config['settings']['lint'])) { // lint disabled in config
return;
}
if (!function_exists('exec')) {
//exec function is disabled #3324
return;
}
exec("php -l " . escapeshellarg($file) . " 2>&1", $output, $code);
if ($code !== 0) {
throw new TestParseException($file, implode("\n", $output));
}
}
public static function load($file)
{
if (PHP_MAJOR_VERSION < 7) {
self::validate($file);
}
try {
self::includeFile($file);
} catch (\ParseError $e) {
throw new TestParseException($file, $e->getMessage(), $e->getLine());
} catch (\Exception $e) {
// file is valid otherwise
}
}
public static function getClassesFromFile($file)
{
$sourceCode = file_get_contents($file);
$classes = [];
$tokens = token_get_all($sourceCode);
$tokenCount = count($tokens);
$namespace = '';
for ($i = 0; $i < $tokenCount; $i++) {
if ($tokens[$i][0] === T_NAMESPACE) {
$namespace = '';
for ($j = $i + 1; $j < $tokenCount; $j++) {
if ($tokens[$j][0] === T_STRING) {
$namespace .= $tokens[$j][1] . '\\';
} else {
if ($tokens[$j] === '{' || $tokens[$j] === ';') {
break;
}
}
}
}
if ($tokens[$i][0] === T_CLASS) {
if (!isset($tokens[$i - 2])) {
$classes[] = $namespace . $tokens[$i + 2][1];
continue;
}
if ($tokens[$i - 2][0] === T_NEW) {
continue;
}
if ($tokens[$i - 1][0] === T_WHITESPACE and $tokens[$i - 2][0] === T_DOUBLE_COLON) {
continue;
}
if ($tokens[$i - 1][0] === T_DOUBLE_COLON) {
continue;
}
$classes[] = $namespace . $tokens[$i + 2][1];
}
}
return $classes;
}
/*
* Include in different scope to prevent included file from affecting $file variable
*/
private static function includeFile($file)
{
include_once $file;
}
/**
* @param $code
* @return mixed
*/
protected function stripComments($code)
{
$code = preg_replace('~\/\/.*?$~m', '', $code); // remove inline comments
$code = preg_replace('~\/*\*.*?\*\/~ms', '', $code);
return $code; // remove block comment
}
protected function matchComments($code)
{
$matches = [];
$comments = '';
$hasLineComment = preg_match_all('~\/\/(.*?)$~m', $code, $matches);
if ($hasLineComment) {
foreach ($matches[1] as $line) {
$comments .= $line."\n";
}
}
$hasBlockComment = preg_match('~\/*\*(.*?)\*\/~ms', $code, $matches);
if ($hasBlockComment) {
$comments .= $matches[1]."\n";
}
return $comments;
}
}

View File

@@ -0,0 +1,3 @@
# Internal Libraries
Various classes that Codeception core and modules are relying on.

View File

@@ -0,0 +1,122 @@
<?php
namespace Codeception\Lib\Shared;
/**
* Common functions for Laravel family
*
* @package Codeception\Lib\Shared
*/
trait LaravelCommon
{
/**
* Add a binding to the Laravel service container.
* (https://laravel.com/docs/master/container)
*
* ``` php
* <?php
* $I->haveBinding('My\Interface', 'My\Implementation');
* ?>
* ```
*
* @param $abstract
* @param $concrete
*/
public function haveBinding($abstract, $concrete)
{
$this->client->haveBinding($abstract, $concrete);
}
/**
* Add a singleton binding to the Laravel service container.
* (https://laravel.com/docs/master/container)
*
* ``` php
* <?php
* $I->haveSingleton('My\Interface', 'My\Singleton');
* ?>
* ```
*
* @param $abstract
* @param $concrete
*/
public function haveSingleton($abstract, $concrete)
{
$this->client->haveBinding($abstract, $concrete, true);
}
/**
* Add a contextual binding to the Laravel service container.
* (https://laravel.com/docs/master/container)
*
* ``` php
* <?php
* $I->haveContextualBinding('My\Class', '$variable', 'value');
*
* // This is similar to the following in your Laravel application
* $app->when('My\Class')
* ->needs('$variable')
* ->give('value');
* ?>
* ```
*
* @param $concrete
* @param $abstract
* @param $implementation
*/
public function haveContextualBinding($concrete, $abstract, $implementation)
{
$this->client->haveContextualBinding($concrete, $abstract, $implementation);
}
/**
* Add an instance binding to the Laravel service container.
* (https://laravel.com/docs/master/container)
*
* ``` php
* <?php
* $I->haveInstance('My\Class', new My\Class());
* ?>
* ```
*
* @param $abstract
* @param $instance
*/
public function haveInstance($abstract, $instance)
{
$this->client->haveInstance($abstract, $instance);
}
/**
* Register a handler than can be used to modify the Laravel application object after it is initialized.
* The Laravel application object will be passed as an argument to the handler.
*
* ``` php
* <?php
* $I->haveApplicationHandler(function($app) {
* $app->make('config')->set(['test_value' => '10']);
* });
* ?>
* ```
*
* @param $handler
*/
public function haveApplicationHandler($handler)
{
$this->client->haveApplicationHandler($handler);
}
/**
* Clear the registered application handlers.
*
* ``` php
* <?php
* $I->clearApplicationHandlers();
* ?>
* ```
*
*/
public function clearApplicationHandlers()
{
$this->client->clearApplicationHandlers();
}
}