Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -0,0 +1,79 @@
<?php
namespace Illuminate\Testing;
use ArrayAccess;
use Illuminate\Testing\Constraints\ArraySubset;
use PHPUnit\Framework\Assert as PHPUnit;
use PHPUnit\Framework\Constraint\DirectoryExists;
use PHPUnit\Framework\Constraint\FileExists;
use PHPUnit\Framework\Constraint\LogicalNot;
use PHPUnit\Framework\Constraint\RegularExpression;
use PHPUnit\Framework\InvalidArgumentException;
/**
* @internal This class is not meant to be used or overwritten outside the framework itself.
*/
abstract class Assert extends PHPUnit
{
/**
* Asserts that an array has a specified subset.
*
* @param \ArrayAccess|array $subset
* @param \ArrayAccess|array $array
* @param bool $checkForIdentity
* @param string $msg
* @return void
*/
public static function assertArraySubset($subset, $array, bool $checkForIdentity = false, string $msg = ''): void
{
if (! (is_array($subset) || $subset instanceof ArrayAccess)) {
throw InvalidArgumentException::create(1, 'array or ArrayAccess');
}
if (! (is_array($array) || $array instanceof ArrayAccess)) {
throw InvalidArgumentException::create(2, 'array or ArrayAccess');
}
$constraint = new ArraySubset($subset, $checkForIdentity);
PHPUnit::assertThat($array, $constraint, $msg);
}
/**
* Asserts that a file does not exist.
*
* @param string $filename
* @param string $message
* @return void
*/
public static function assertFileDoesNotExist(string $filename, string $message = ''): void
{
static::assertThat($filename, new LogicalNot(new FileExists), $message);
}
/**
* Asserts that a directory does not exist.
*
* @param string $directory
* @param string $message
* @return void
*/
public static function assertDirectoryDoesNotExist(string $directory, string $message = ''): void
{
static::assertThat($directory, new LogicalNot(new DirectoryExists), $message);
}
/**
* Asserts that a string matches a given regular expression.
*
* @param string $pattern
* @param string $string
* @param string $message
* @return void
*/
public static function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void
{
static::assertThat($string, new RegularExpression($pattern), $message);
}
}

View File

@@ -0,0 +1,411 @@
<?php
namespace Illuminate\Testing;
use ArrayAccess;
use Closure;
use Countable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Testing\Assert as PHPUnit;
use JsonSerializable;
class AssertableJsonString implements ArrayAccess, Countable
{
/**
* The original encoded json.
*
* @var \Illuminate\Contracts\Support\Jsonable|\JsonSerializable|array|string
*/
public $json;
/**
* The decoded json contents.
*
* @var array|null
*/
protected $decoded;
/**
* Create a new assertable JSON string instance.
*
* @param \Illuminate\Contracts\Support\Jsonable|\JsonSerializable|array|string $jsonable
* @return void
*/
public function __construct($jsonable)
{
$this->json = $jsonable;
if ($jsonable instanceof JsonSerializable) {
$this->decoded = $jsonable->jsonSerialize();
} elseif ($jsonable instanceof Jsonable) {
$this->decoded = json_decode($jsonable->toJson(), true);
} elseif (is_array($jsonable)) {
$this->decoded = $jsonable;
} else {
$this->decoded = json_decode($jsonable, true);
}
}
/**
* Validate and return the decoded response JSON.
*
* @param string|null $key
* @return mixed
*/
public function json($key = null)
{
return data_get($this->decoded, $key);
}
/**
* Assert that the response JSON has the expected count of items at the given key.
*
* @param int $count
* @param string|null $key
* @return $this
*/
public function assertCount(int $count, $key = null)
{
if (! is_null($key)) {
PHPUnit::assertCount(
$count, data_get($this->decoded, $key),
"Failed to assert that the response count matched the expected {$count}"
);
return $this;
}
PHPUnit::assertCount($count,
$this->decoded,
"Failed to assert that the response count matched the expected {$count}"
);
return $this;
}
/**
* Assert that the response has the exact given JSON.
*
* @param array $data
* @return $this
*/
public function assertExact(array $data)
{
$actual = $this->reorderAssocKeys((array) $this->decoded);
$expected = $this->reorderAssocKeys($data);
PHPUnit::assertEquals(
json_encode($expected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
json_encode($actual, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
return $this;
}
/**
* Assert that the response has the similar JSON as given.
*
* @param array $data
* @return $this
*/
public function assertSimilar(array $data)
{
$actual = json_encode(
Arr::sortRecursive((array) $this->decoded),
JSON_UNESCAPED_UNICODE
);
PHPUnit::assertEquals(json_encode(Arr::sortRecursive($data), JSON_UNESCAPED_UNICODE), $actual);
return $this;
}
/**
* Assert that the response contains the given JSON fragment.
*
* @param array $data
* @return $this
*/
public function assertFragment(array $data)
{
$actual = json_encode(
Arr::sortRecursive((array) $this->decoded),
JSON_UNESCAPED_UNICODE
);
foreach (Arr::sortRecursive($data) as $key => $value) {
$expected = $this->jsonSearchStrings($key, $value);
PHPUnit::assertTrue(
Str::contains($actual, $expected),
'Unable to find JSON fragment: '.PHP_EOL.PHP_EOL.
'['.json_encode([$key => $value], JSON_UNESCAPED_UNICODE).']'.PHP_EOL.PHP_EOL.
'within'.PHP_EOL.PHP_EOL.
"[{$actual}]."
);
}
return $this;
}
/**
* Assert that the response does not contain the given JSON fragment.
*
* @param array $data
* @param bool $exact
* @return $this
*/
public function assertMissing(array $data, $exact = false)
{
if ($exact) {
return $this->assertMissingExact($data);
}
$actual = json_encode(
Arr::sortRecursive((array) $this->decoded),
JSON_UNESCAPED_UNICODE
);
foreach (Arr::sortRecursive($data) as $key => $value) {
$unexpected = $this->jsonSearchStrings($key, $value);
PHPUnit::assertFalse(
Str::contains($actual, $unexpected),
'Found unexpected JSON fragment: '.PHP_EOL.PHP_EOL.
'['.json_encode([$key => $value], JSON_UNESCAPED_UNICODE).']'.PHP_EOL.PHP_EOL.
'within'.PHP_EOL.PHP_EOL.
"[{$actual}]."
);
}
return $this;
}
/**
* Assert that the response does not contain the exact JSON fragment.
*
* @param array $data
* @return $this
*/
public function assertMissingExact(array $data)
{
$actual = json_encode(
Arr::sortRecursive((array) $this->decoded),
JSON_UNESCAPED_UNICODE
);
foreach (Arr::sortRecursive($data) as $key => $value) {
$unexpected = $this->jsonSearchStrings($key, $value);
if (! Str::contains($actual, $unexpected)) {
return $this;
}
}
PHPUnit::fail(
'Found unexpected JSON fragment: '.PHP_EOL.PHP_EOL.
'['.json_encode($data, JSON_UNESCAPED_UNICODE).']'.PHP_EOL.PHP_EOL.
'within'.PHP_EOL.PHP_EOL.
"[{$actual}]."
);
return $this;
}
/**
* Assert that the response does not contain the given path.
*
* @param string $path
* @return $this
*/
public function assertMissingPath($path)
{
PHPUnit::assertFalse(Arr::has($this->json(), $path));
return $this;
}
/**
* Assert that the expected value and type exists at the given path in the response.
*
* @param string $path
* @param mixed $expect
* @return $this
*/
public function assertPath($path, $expect)
{
if ($expect instanceof Closure) {
PHPUnit::assertTrue($expect($this->json($path)));
} else {
PHPUnit::assertSame($expect, $this->json($path));
}
return $this;
}
/**
* Assert that the response has a given JSON structure.
*
* @param array|null $structure
* @param array|null $responseData
* @return $this
*/
public function assertStructure(array $structure = null, $responseData = null)
{
if (is_null($structure)) {
return $this->assertSimilar($this->decoded);
}
if (! is_null($responseData)) {
return (new static($responseData))->assertStructure($structure);
}
foreach ($structure as $key => $value) {
if (is_array($value) && $key === '*') {
PHPUnit::assertIsArray($this->decoded);
foreach ($this->decoded as $responseDataItem) {
$this->assertStructure($structure['*'], $responseDataItem);
}
} elseif (is_array($value)) {
PHPUnit::assertArrayHasKey($key, $this->decoded);
$this->assertStructure($structure[$key], $this->decoded[$key]);
} else {
PHPUnit::assertArrayHasKey($value, $this->decoded);
}
}
return $this;
}
/**
* Assert that the response is a superset of the given JSON.
*
* @param array $data
* @param bool $strict
* @return $this
*/
public function assertSubset(array $data, $strict = false)
{
PHPUnit::assertArraySubset(
$data, $this->decoded, $strict, $this->assertJsonMessage($data)
);
return $this;
}
/**
* Reorder associative array keys to make it easy to compare arrays.
*
* @param array $data
* @return array
*/
protected function reorderAssocKeys(array $data)
{
$data = Arr::dot($data);
ksort($data);
$result = [];
foreach ($data as $key => $value) {
Arr::set($result, $key, $value);
}
return $result;
}
/**
* Get the assertion message for assertJson.
*
* @param array $data
* @return string
*/
protected function assertJsonMessage(array $data)
{
$expected = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$actual = json_encode($this->decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return 'Unable to find JSON: '.PHP_EOL.PHP_EOL.
"[{$expected}]".PHP_EOL.PHP_EOL.
'within response JSON:'.PHP_EOL.PHP_EOL.
"[{$actual}].".PHP_EOL.PHP_EOL;
}
/**
* Get the strings we need to search for when examining the JSON.
*
* @param string $key
* @param string $value
* @return array
*/
protected function jsonSearchStrings($key, $value)
{
$needle = Str::substr(json_encode([$key => $value], JSON_UNESCAPED_UNICODE), 1, -1);
return [
$needle.']',
$needle.'}',
$needle.',',
];
}
/**
* Get the total number of items in the underlying JSON array.
*
* @return int
*/
public function count(): int
{
return count($this->decoded);
}
/**
* Determine whether an offset exists.
*
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->decoded[$offset]);
}
/**
* Get the value at the given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->decoded[$offset];
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value): void
{
$this->decoded[$offset] = $value;
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*/
public function offsetUnset($offset): void
{
unset($this->decoded[$offset]);
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Illuminate\Testing\Concerns;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\Facades\Schema;
trait TestDatabases
{
/**
* Indicates if the test database schema is up to date.
*
* @var bool
*/
protected static $schemaIsUpToDate = false;
/**
* Boot a test database.
*
* @return void
*/
protected function bootTestDatabase()
{
ParallelTesting::setUpProcess(function () {
$this->whenNotUsingInMemoryDatabase(function ($database) {
if (ParallelTesting::option('recreate_databases')) {
Schema::dropDatabaseIfExists(
$this->testDatabase($database)
);
}
});
});
ParallelTesting::setUpTestCase(function ($testCase) {
$uses = array_flip(class_uses_recursive(get_class($testCase)));
$databaseTraits = [
Testing\DatabaseMigrations::class,
Testing\DatabaseTransactions::class,
Testing\RefreshDatabase::class,
];
if (Arr::hasAny($uses, $databaseTraits) && ! ParallelTesting::option('without_databases')) {
$this->whenNotUsingInMemoryDatabase(function ($database) use ($uses) {
[$testDatabase, $created] = $this->ensureTestDatabaseExists($database);
$this->switchToDatabase($testDatabase);
if (isset($uses[Testing\DatabaseTransactions::class])) {
$this->ensureSchemaIsUpToDate();
}
if ($created) {
ParallelTesting::callSetUpTestDatabaseCallbacks($testDatabase);
}
});
}
});
ParallelTesting::tearDownProcess(function () {
$this->whenNotUsingInMemoryDatabase(function ($database) {
if (ParallelTesting::option('drop_databases')) {
Schema::dropDatabaseIfExists(
$this->testDatabase($database)
);
}
});
});
}
/**
* Ensure a test database exists and returns its name.
*
* @param string $database
* @return array
*/
protected function ensureTestDatabaseExists($database)
{
$testDatabase = $this->testDatabase($database);
try {
$this->usingDatabase($testDatabase, function () {
Schema::hasTable('dummy');
});
} catch (QueryException $e) {
$this->usingDatabase($database, function () use ($testDatabase) {
Schema::dropDatabaseIfExists($testDatabase);
Schema::createDatabase($testDatabase);
});
return [$testDatabase, true];
}
return [$testDatabase, false];
}
/**
* Ensure the current database test schema is up to date.
*
* @return void
*/
protected function ensureSchemaIsUpToDate()
{
if (! static::$schemaIsUpToDate) {
Artisan::call('migrate');
static::$schemaIsUpToDate = true;
}
}
/**
* Runs the given callable using the given database.
*
* @param string $database
* @param callable $callable
* @return void
*/
protected function usingDatabase($database, $callable)
{
$original = DB::getConfig('database');
try {
$this->switchToDatabase($database);
$callable();
} finally {
$this->switchToDatabase($original);
}
}
/**
* Apply the given callback when tests are not using in memory database.
*
* @param callable $callback
* @return void
*/
protected function whenNotUsingInMemoryDatabase($callback)
{
$database = DB::getConfig('database');
if ($database !== ':memory:') {
$callback($database);
}
}
/**
* Switch to the given database.
*
* @param string $database
* @return void
*/
protected function switchToDatabase($database)
{
DB::purge();
$default = config('database.default');
$url = config("database.connections.{$default}.url");
if ($url) {
config()->set(
"database.connections.{$default}.url",
preg_replace('/^(.*)(\/[\w-]*)(\??.*)$/', "$1/{$database}$3", $url),
);
} else {
config()->set(
"database.connections.{$default}.database",
$database,
);
}
}
/**
* Returns the test database name.
*
* @return string
*/
protected function testDatabase($database)
{
$token = ParallelTesting::token();
return "{$database}_test_{$token}";
}
}

View File

@@ -0,0 +1,279 @@
<?php
namespace Illuminate\Testing\Constraints;
use ArrayObject;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Runner\Version;
use SebastianBergmann\Comparator\ComparisonFailure;
use Traversable;
if (class_exists(Version::class) && (int) Version::series()[0] >= 9) {
/**
* @internal This class is not meant to be used or overwritten outside the framework itself.
*/
final class ArraySubset extends Constraint
{
/**
* @var iterable
*/
private $subset;
/**
* @var bool
*/
private $strict;
/**
* Create a new array subset constraint instance.
*
* @param iterable $subset
* @param bool $strict
* @return void
*/
public function __construct(iterable $subset, bool $strict = false)
{
$this->strict = $strict;
$this->subset = $subset;
}
/**
* Evaluates the constraint for parameter $other.
*
* If $returnResult is set to false (the default), an exception is thrown
* in case of a failure. null is returned otherwise.
*
* If $returnResult is true, the result of the evaluation is returned as
* a boolean value instead: true in case of success, false in case of a
* failure.
*
* @param mixed $other
* @param string $description
* @param bool $returnResult
* @return bool|null
*
* @throws \PHPUnit\Framework\ExpectationFailedException
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function evaluate($other, string $description = '', bool $returnResult = false): ?bool
{
// type cast $other & $this->subset as an array to allow
// support in standard array functions.
$other = $this->toArray($other);
$this->subset = $this->toArray($this->subset);
$patched = array_replace_recursive($other, $this->subset);
if ($this->strict) {
$result = $other === $patched;
} else {
$result = $other == $patched;
}
if ($returnResult) {
return $result;
}
if (! $result) {
$f = new ComparisonFailure(
$patched,
$other,
var_export($patched, true),
var_export($other, true)
);
$this->fail($other, $description, $f);
}
return null;
}
/**
* Returns a string representation of the constraint.
*
* @return string
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function toString(): string
{
return 'has the subset '.$this->exporter()->export($this->subset);
}
/**
* Returns the description of the failure.
*
* The beginning of failure messages is "Failed asserting that" in most
* cases. This method should return the second part of that sentence.
*
* @param mixed $other
* @return string
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
protected function failureDescription($other): string
{
return 'an array '.$this->toString();
}
/**
* Returns the description of the failure.
*
* The beginning of failure messages is "Failed asserting that" in most
* cases. This method should return the second part of that sentence.
*
* @param iterable $other
* @return array
*/
private function toArray(iterable $other): array
{
if (is_array($other)) {
return $other;
}
if ($other instanceof ArrayObject) {
return $other->getArrayCopy();
}
if ($other instanceof Traversable) {
return iterator_to_array($other);
}
// Keep BC even if we know that array would not be the expected one
return (array) $other;
}
}
} else {
/**
* @internal This class is not meant to be used or overwritten outside the framework itself.
*/
final class ArraySubset extends Constraint
{
/**
* @var iterable
*/
private $subset;
/**
* @var bool
*/
private $strict;
/**
* Create a new array subset constraint instance.
*
* @param iterable $subset
* @param bool $strict
* @return void
*/
public function __construct(iterable $subset, bool $strict = false)
{
$this->strict = $strict;
$this->subset = $subset;
}
/**
* Evaluates the constraint for parameter $other.
*
* If $returnResult is set to false (the default), an exception is thrown
* in case of a failure. null is returned otherwise.
*
* If $returnResult is true, the result of the evaluation is returned as
* a boolean value instead: true in case of success, false in case of a
* failure.
*
* @param mixed $other
* @param string $description
* @param bool $returnResult
* @return bool|null
*
* @throws \PHPUnit\Framework\ExpectationFailedException
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function evaluate($other, string $description = '', bool $returnResult = false)
{
// type cast $other & $this->subset as an array to allow
// support in standard array functions.
$other = $this->toArray($other);
$this->subset = $this->toArray($this->subset);
$patched = array_replace_recursive($other, $this->subset);
if ($this->strict) {
$result = $other === $patched;
} else {
$result = $other == $patched;
}
if ($returnResult) {
return $result;
}
if (! $result) {
$f = new ComparisonFailure(
$patched,
$other,
var_export($patched, true),
var_export($other, true)
);
$this->fail($other, $description, $f);
}
}
/**
* Returns a string representation of the constraint.
*
* @return string
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function toString(): string
{
return 'has the subset '.$this->exporter()->export($this->subset);
}
/**
* Returns the description of the failure.
*
* The beginning of failure messages is "Failed asserting that" in most
* cases. This method should return the second part of that sentence.
*
* @param mixed $other
* @return string
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
protected function failureDescription($other): string
{
return 'an array '.$this->toString();
}
/**
* Returns the description of the failure.
*
* The beginning of failure messages is "Failed asserting that" in most
* cases. This method should return the second part of that sentence.
*
* @param iterable $other
* @return array
*/
private function toArray(iterable $other): array
{
if (is_array($other)) {
return $other;
}
if ($other instanceof ArrayObject) {
return $other->getArrayCopy();
}
if ($other instanceof Traversable) {
return iterator_to_array($other);
}
// Keep BC even if we know that array would not be the expected one
return (array) $other;
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Illuminate\Testing\Constraints;
use Illuminate\Database\Connection;
use PHPUnit\Framework\Constraint\Constraint;
use ReflectionClass;
class CountInDatabase extends Constraint
{
/**
* The database connection.
*
* @var \Illuminate\Database\Connection
*/
protected $database;
/**
* The expected table entries count that will be checked against the actual count.
*
* @var int
*/
protected $expectedCount;
/**
* The actual table entries count that will be checked against the expected count.
*
* @var int
*/
protected $actualCount;
/**
* Create a new constraint instance.
*
* @param \Illuminate\Database\Connection $database
* @param int $expectedCount
* @return void
*/
public function __construct(Connection $database, int $expectedCount)
{
$this->expectedCount = $expectedCount;
$this->database = $database;
}
/**
* Check if the expected and actual count are equal.
*
* @param string $table
* @return bool
*/
public function matches($table): bool
{
$this->actualCount = $this->database->table($table)->count();
return $this->actualCount === $this->expectedCount;
}
/**
* Get the description of the failure.
*
* @param string $table
* @return string
*/
public function failureDescription($table): string
{
return sprintf(
"table [%s] matches expected entries count of %s. Entries found: %s.\n",
$table, $this->expectedCount, $this->actualCount
);
}
/**
* Get a string representation of the object.
*
* @param int $options
* @return string
*/
public function toString($options = 0): string
{
return (new ReflectionClass($this))->name;
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Illuminate\Testing\Constraints;
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Expression;
use PHPUnit\Framework\Constraint\Constraint;
class HasInDatabase extends Constraint
{
/**
* Number of records that will be shown in the console in case of failure.
*
* @var int
*/
protected $show = 3;
/**
* The database connection.
*
* @var \Illuminate\Database\Connection
*/
protected $database;
/**
* The data that will be used to narrow the search in the database table.
*
* @var array
*/
protected $data;
/**
* Create a new constraint instance.
*
* @param \Illuminate\Database\Connection $database
* @param array $data
* @return void
*/
public function __construct(Connection $database, array $data)
{
$this->data = $data;
$this->database = $database;
}
/**
* Check if the data is found in the given table.
*
* @param string $table
* @return bool
*/
public function matches($table): bool
{
return $this->database->table($table)->where($this->data)->count() > 0;
}
/**
* Get the description of the failure.
*
* @param string $table
* @return string
*/
public function failureDescription($table): string
{
return sprintf(
"a row in the table [%s] matches the attributes %s.\n\n%s",
$table, $this->toString(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), $this->getAdditionalInfo($table)
);
}
/**
* Get additional info about the records found in the database table.
*
* @param string $table
* @return string
*/
protected function getAdditionalInfo($table)
{
$query = $this->database->table($table);
$similarResults = $query->where(
array_key_first($this->data),
$this->data[array_key_first($this->data)]
)->select(array_keys($this->data))->limit($this->show)->get();
if ($similarResults->isNotEmpty()) {
$description = 'Found similar results: '.json_encode($similarResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} else {
$query = $this->database->table($table);
$results = $query->select(array_keys($this->data))->limit($this->show)->get();
if ($results->isEmpty()) {
return 'The table is empty';
}
$description = 'Found: '.json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
if ($query->count() > $this->show) {
$description .= sprintf(' and %s others', $query->count() - $this->show);
}
return $description;
}
/**
* Get a string representation of the object.
*
* @param int $options
* @return string
*/
public function toString($options = 0): string
{
foreach ($this->data as $key => $data) {
$output[$key] = $data instanceof Expression ? (string) $data : $data;
}
return json_encode($output ?? [], $options);
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Illuminate\Testing\Constraints;
use Illuminate\Database\Connection;
use PHPUnit\Framework\Constraint\Constraint;
class NotSoftDeletedInDatabase extends Constraint
{
/**
* Number of records that will be shown in the console in case of failure.
*
* @var int
*/
protected $show = 3;
/**
* The database connection.
*
* @var \Illuminate\Database\Connection
*/
protected $database;
/**
* The data that will be used to narrow the search in the database table.
*
* @var array
*/
protected $data;
/**
* The name of the column that indicates soft deletion has occurred.
*
* @var string
*/
protected $deletedAtColumn;
/**
* Create a new constraint instance.
*
* @param \Illuminate\Database\Connection $database
* @param array $data
* @param string $deletedAtColumn
* @return void
*/
public function __construct(Connection $database, array $data, string $deletedAtColumn)
{
$this->database = $database;
$this->data = $data;
$this->deletedAtColumn = $deletedAtColumn;
}
/**
* Check if the data is found in the given table.
*
* @param string $table
* @return bool
*/
public function matches($table): bool
{
return $this->database->table($table)
->where($this->data)
->whereNull($this->deletedAtColumn)
->count() > 0;
}
/**
* Get the description of the failure.
*
* @param string $table
* @return string
*/
public function failureDescription($table): string
{
return sprintf(
"any existing row in the table [%s] matches the attributes %s.\n\n%s",
$table, $this->toString(), $this->getAdditionalInfo($table)
);
}
/**
* Get additional info about the records found in the database table.
*
* @param string $table
* @return string
*/
protected function getAdditionalInfo($table)
{
$query = $this->database->table($table);
$results = $query->limit($this->show)->get();
if ($results->isEmpty()) {
return 'The table is empty';
}
$description = 'Found: '.json_encode($results, JSON_PRETTY_PRINT);
if ($query->count() > $this->show) {
$description .= sprintf(' and %s others', $query->count() - $this->show);
}
return $description;
}
/**
* Get a string representation of the object.
*
* @return string
*/
public function toString(): string
{
return json_encode($this->data);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Illuminate\Testing\Constraints;
use PHPUnit\Framework\Constraint\Constraint;
use ReflectionClass;
class SeeInOrder extends Constraint
{
/**
* The string under validation.
*
* @var string
*/
protected $content;
/**
* The last value that failed to pass validation.
*
* @var string
*/
protected $failedValue;
/**
* Create a new constraint instance.
*
* @param string $content
* @return void
*/
public function __construct($content)
{
$this->content = $content;
}
/**
* Determine if the rule passes validation.
*
* @param array $values
* @return bool
*/
public function matches($values): bool
{
$position = 0;
foreach ($values as $value) {
if (empty($value)) {
continue;
}
$valuePosition = mb_strpos($this->content, $value, $position);
if ($valuePosition === false || $valuePosition < $position) {
$this->failedValue = $value;
return false;
}
$position = $valuePosition + mb_strlen($value);
}
return true;
}
/**
* Get the description of the failure.
*
* @param array $values
* @return string
*/
public function failureDescription($values): string
{
return sprintf(
'Failed asserting that \'%s\' contains "%s" in specified order.',
$this->content,
$this->failedValue
);
}
/**
* Get a string representation of the object.
*
* @return string
*/
public function toString(): string
{
return (new ReflectionClass($this))->name;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Illuminate\Testing\Constraints;
use Illuminate\Database\Connection;
use PHPUnit\Framework\Constraint\Constraint;
class SoftDeletedInDatabase extends Constraint
{
/**
* Number of records that will be shown in the console in case of failure.
*
* @var int
*/
protected $show = 3;
/**
* The database connection.
*
* @var \Illuminate\Database\Connection
*/
protected $database;
/**
* The data that will be used to narrow the search in the database table.
*
* @var array
*/
protected $data;
/**
* The name of the column that indicates soft deletion has occurred.
*
* @var string
*/
protected $deletedAtColumn;
/**
* Create a new constraint instance.
*
* @param \Illuminate\Database\Connection $database
* @param array $data
* @param string $deletedAtColumn
* @return void
*/
public function __construct(Connection $database, array $data, string $deletedAtColumn)
{
$this->data = $data;
$this->database = $database;
$this->deletedAtColumn = $deletedAtColumn;
}
/**
* Check if the data is found in the given table.
*
* @param string $table
* @return bool
*/
public function matches($table): bool
{
return $this->database->table($table)
->where($this->data)
->whereNotNull($this->deletedAtColumn)
->count() > 0;
}
/**
* Get the description of the failure.
*
* @param string $table
* @return string
*/
public function failureDescription($table): string
{
return sprintf(
"any soft deleted row in the table [%s] matches the attributes %s.\n\n%s",
$table, $this->toString(), $this->getAdditionalInfo($table)
);
}
/**
* Get additional info about the records found in the database table.
*
* @param string $table
* @return string
*/
protected function getAdditionalInfo($table)
{
$query = $this->database->table($table);
$results = $query->limit($this->show)->get();
if ($results->isEmpty()) {
return 'The table is empty';
}
$description = 'Found: '.json_encode($results, JSON_PRETTY_PRINT);
if ($query->count() > $this->show) {
$description .= sprintf(' and %s others', $query->count() - $this->show);
}
return $description;
}
/**
* Get a string representation of the object.
*
* @return string
*/
public function toString(): string
{
return json_encode($this->data);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Illuminate\Testing\Fluent;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\Tappable;
use Illuminate\Testing\AssertableJsonString;
use PHPUnit\Framework\Assert as PHPUnit;
class AssertableJson implements Arrayable
{
use Concerns\Has,
Concerns\Matching,
Concerns\Debugging,
Concerns\Interaction,
Macroable,
Tappable;
/**
* The properties in the current scope.
*
* @var array
*/
private $props;
/**
* The "dot" path to the current scope.
*
* @var string|null
*/
private $path;
/**
* Create a new fluent, assertable JSON data instance.
*
* @param array $props
* @param string|null $path
* @return void
*/
protected function __construct(array $props, string $path = null)
{
$this->path = $path;
$this->props = $props;
}
/**
* Compose the absolute "dot" path to the given key.
*
* @param string $key
* @return string
*/
protected function dotPath(string $key = ''): string
{
if (is_null($this->path)) {
return $key;
}
return rtrim(implode('.', [$this->path, $key]), '.');
}
/**
* Retrieve a prop within the current scope using "dot" notation.
*
* @param string|null $key
* @return mixed
*/
protected function prop(string $key = null)
{
return Arr::get($this->props, $key);
}
/**
* Instantiate a new "scope" at the path of the given key.
*
* @param string $key
* @param \Closure $callback
* @return $this
*/
protected function scope(string $key, Closure $callback): self
{
$props = $this->prop($key);
$path = $this->dotPath($key);
PHPUnit::assertIsArray($props, sprintf('Property [%s] is not scopeable.', $path));
$scope = new static($props, $path);
$callback($scope);
$scope->interacted();
return $this;
}
/**
* Instantiate a new "scope" on the first child element.
*
* @param \Closure $callback
* @return $this
*/
public function first(Closure $callback): self
{
$props = $this->prop();
$path = $this->dotPath();
PHPUnit::assertNotEmpty($props, $path === ''
? 'Cannot scope directly onto the first element of the root level because it is empty.'
: sprintf('Cannot scope directly onto the first element of property [%s] because it is empty.', $path)
);
$key = array_keys($props)[0];
$this->interactsWith($key);
return $this->scope($key, $callback);
}
/**
* Instantiate a new "scope" on each child element.
*
* @param \Closure $callback
* @return $this
*/
public function each(Closure $callback): self
{
$props = $this->prop();
$path = $this->dotPath();
PHPUnit::assertNotEmpty($props, $path === ''
? 'Cannot scope directly onto each element of the root level because it is empty.'
: sprintf('Cannot scope directly onto each element of property [%s] because it is empty.', $path)
);
foreach (array_keys($props) as $key) {
$this->interactsWith($key);
$this->scope($key, $callback);
}
return $this;
}
/**
* Create a new instance from an array.
*
* @param array $data
* @return static
*/
public static function fromArray(array $data): self
{
return new static($data);
}
/**
* Create a new instance from an AssertableJsonString.
*
* @param \Illuminate\Testing\AssertableJsonString $json
* @return static
*/
public static function fromAssertableJsonString(AssertableJsonString $json): self
{
return static::fromArray($json->json());
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return $this->props;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Illuminate\Testing\Fluent\Concerns;
trait Debugging
{
/**
* Dumps the given props.
*
* @param string|null $prop
* @return $this
*/
public function dump(string $prop = null): self
{
dump($this->prop($prop));
return $this;
}
/**
* Dumps the given props and exits.
*
* @param string|null $prop
* @return never
*/
public function dd(string $prop = null): void
{
dd($this->prop($prop));
}
/**
* Retrieve a prop within the current scope using "dot" notation.
*
* @param string|null $key
* @return mixed
*/
abstract protected function prop(string $key = null);
}

View File

@@ -0,0 +1,213 @@
<?php
namespace Illuminate\Testing\Fluent\Concerns;
use Closure;
use Illuminate\Support\Arr;
use PHPUnit\Framework\Assert as PHPUnit;
trait Has
{
/**
* Assert that the prop is of the expected size.
*
* @param string|int $key
* @param int|null $length
* @return $this
*/
public function count($key, int $length = null): self
{
if (is_null($length)) {
$path = $this->dotPath();
PHPUnit::assertCount(
$key,
$this->prop(),
$path
? sprintf('Property [%s] does not have the expected size.', $path)
: sprintf('Root level does not have the expected size.')
);
return $this;
}
PHPUnit::assertCount(
$length,
$this->prop($key),
sprintf('Property [%s] does not have the expected size.', $this->dotPath($key))
);
return $this;
}
/**
* Ensure that the given prop exists.
*
* @param string|int $key
* @param int|\Closure|null $length
* @param \Closure|null $callback
* @return $this
*/
public function has($key, $length = null, Closure $callback = null): self
{
$prop = $this->prop();
if (is_int($key) && is_null($length)) {
return $this->count($key);
}
PHPUnit::assertTrue(
Arr::has($prop, $key),
sprintf('Property [%s] does not exist.', $this->dotPath($key))
);
$this->interactsWith($key);
if (! is_null($callback)) {
return $this->has($key, function (self $scope) use ($length, $callback) {
return $scope
->tap(function (self $scope) use ($length) {
if (! is_null($length)) {
$scope->count($length);
}
})
->first($callback)
->etc();
});
}
if (is_callable($length)) {
return $this->scope($key, $length);
}
if (! is_null($length)) {
return $this->count($key, $length);
}
return $this;
}
/**
* Assert that all of the given props exist.
*
* @param array|string $key
* @return $this
*/
public function hasAll($key): self
{
$keys = is_array($key) ? $key : func_get_args();
foreach ($keys as $prop => $count) {
if (is_int($prop)) {
$this->has($count);
} else {
$this->has($prop, $count);
}
}
return $this;
}
/**
* Assert that at least one of the given props exists.
*
* @param array|string $key
* @return $this
*/
public function hasAny($key): self
{
$keys = is_array($key) ? $key : func_get_args();
PHPUnit::assertTrue(
Arr::hasAny($this->prop(), $keys),
sprintf('None of properties [%s] exist.', implode(', ', $keys))
);
foreach ($keys as $key) {
$this->interactsWith($key);
}
return $this;
}
/**
* Assert that none of the given props exist.
*
* @param array|string $key
* @return $this
*/
public function missingAll($key): self
{
$keys = is_array($key) ? $key : func_get_args();
foreach ($keys as $prop) {
$this->missing($prop);
}
return $this;
}
/**
* Assert that the given prop does not exist.
*
* @param string $key
* @return $this
*/
public function missing(string $key): self
{
PHPUnit::assertNotTrue(
Arr::has($this->prop(), $key),
sprintf('Property [%s] was found while it was expected to be missing.', $this->dotPath($key))
);
return $this;
}
/**
* Compose the absolute "dot" path to the given key.
*
* @param string $key
* @return string
*/
abstract protected function dotPath(string $key = ''): string;
/**
* Marks the property as interacted.
*
* @param string $key
* @return void
*/
abstract protected function interactsWith(string $key): void;
/**
* Retrieve a prop within the current scope using "dot" notation.
*
* @param string|null $key
* @return mixed
*/
abstract protected function prop(string $key = null);
/**
* Instantiate a new "scope" at the path of the given key.
*
* @param string $key
* @param \Closure $callback
* @return $this
*/
abstract protected function scope(string $key, Closure $callback);
/**
* Disables the interaction check.
*
* @return $this
*/
abstract public function etc();
/**
* Instantiate a new "scope" on the first element.
*
* @param \Closure $callback
* @return $this
*/
abstract public function first(Closure $callback);
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Illuminate\Testing\Fluent\Concerns;
use Illuminate\Support\Str;
use PHPUnit\Framework\Assert as PHPUnit;
trait Interaction
{
/**
* The list of interacted properties.
*
* @var array
*/
protected $interacted = [];
/**
* Marks the property as interacted.
*
* @param string $key
* @return void
*/
protected function interactsWith(string $key): void
{
$prop = Str::before($key, '.');
if (! in_array($prop, $this->interacted, true)) {
$this->interacted[] = $prop;
}
}
/**
* Asserts that all properties have been interacted with.
*
* @return void
*/
public function interacted(): void
{
PHPUnit::assertSame(
[],
array_diff(array_keys($this->prop()), $this->interacted),
$this->path
? sprintf('Unexpected properties were found in scope [%s].', $this->path)
: 'Unexpected properties were found on the root level.'
);
}
/**
* Disables the interaction check.
*
* @return $this
*/
public function etc(): self
{
$this->interacted = array_keys($this->prop());
return $this;
}
/**
* Retrieve a prop within the current scope using "dot" notation.
*
* @param string|null $key
* @return mixed
*/
abstract protected function prop(string $key = null);
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Illuminate\Testing\Fluent\Concerns;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use PHPUnit\Framework\Assert as PHPUnit;
trait Matching
{
/**
* Asserts that the property matches the expected value.
*
* @param string $key
* @param mixed|\Closure $expected
* @return $this
*/
public function where(string $key, $expected): self
{
$this->has($key);
$actual = $this->prop($key);
if ($expected instanceof Closure) {
PHPUnit::assertTrue(
$expected(is_array($actual) ? Collection::make($actual) : $actual),
sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key))
);
return $this;
}
if ($expected instanceof Arrayable) {
$expected = $expected->toArray();
}
$this->ensureSorted($expected);
$this->ensureSorted($actual);
PHPUnit::assertSame(
$expected,
$actual,
sprintf('Property [%s] does not match the expected value.', $this->dotPath($key))
);
return $this;
}
/**
* Asserts that the property does not match the expected value.
*
* @param string $key
* @param mixed|\Closure $expected
* @return $this
*/
public function whereNot(string $key, $expected): self
{
$this->has($key);
$actual = $this->prop($key);
if ($expected instanceof Closure) {
PHPUnit::assertFalse(
$expected(is_array($actual) ? Collection::make($actual) : $actual),
sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key))
);
return $this;
}
if ($expected instanceof Arrayable) {
$expected = $expected->toArray();
}
$this->ensureSorted($expected);
$this->ensureSorted($actual);
PHPUnit::assertNotSame(
$expected,
$actual,
sprintf(
'Property [%s] contains a value that should be missing: [%s, %s]',
$this->dotPath($key),
$key,
$expected
)
);
return $this;
}
/**
* Asserts that all properties match their expected values.
*
* @param array $bindings
* @return $this
*/
public function whereAll(array $bindings): self
{
foreach ($bindings as $key => $value) {
$this->where($key, $value);
}
return $this;
}
/**
* Asserts that the property is of the expected type.
*
* @param string $key
* @param string|array $expected
* @return $this
*/
public function whereType(string $key, $expected): self
{
$this->has($key);
$actual = $this->prop($key);
if (! is_array($expected)) {
$expected = explode('|', $expected);
}
PHPUnit::assertContains(
strtolower(gettype($actual)),
$expected,
sprintf('Property [%s] is not of expected type [%s].', $this->dotPath($key), implode('|', $expected))
);
return $this;
}
/**
* Asserts that all properties are of their expected types.
*
* @param array $bindings
* @return $this
*/
public function whereAllType(array $bindings): self
{
foreach ($bindings as $key => $value) {
$this->whereType($key, $value);
}
return $this;
}
/**
* Asserts that the property contains the expected values.
*
* @param string $key
* @param mixed $expected
* @return $this
*/
public function whereContains(string $key, $expected)
{
$actual = Collection::make(
$this->prop($key) ?? $this->prop()
);
$missing = Collection::make($expected)->reject(function ($search) use ($key, $actual) {
if ($actual->containsStrict($key, $search)) {
return true;
}
return $actual->containsStrict($search);
});
if ($missing->whereInstanceOf('Closure')->isNotEmpty()) {
PHPUnit::assertEmpty(
$missing->toArray(),
sprintf(
'Property [%s] does not contain a value that passes the truth test within the given closure.',
$key,
)
);
} else {
PHPUnit::assertEmpty(
$missing->toArray(),
sprintf(
'Property [%s] does not contain [%s].',
$key,
implode(', ', array_values($missing->toArray()))
)
);
}
return $this;
}
/**
* Ensures that all properties are sorted the same way, recursively.
*
* @param mixed $value
* @return void
*/
protected function ensureSorted(&$value): void
{
if (! is_array($value)) {
return;
}
foreach ($value as &$arg) {
$this->ensureSorted($arg);
}
ksort($value);
}
/**
* Compose the absolute "dot" path to the given key.
*
* @param string $key
* @return string
*/
abstract protected function dotPath(string $key = ''): string;
/**
* Ensure that the given prop exists.
*
* @param string $key
* @param null $value
* @param \Closure|null $scope
* @return $this
*/
abstract public function has(string $key, $value = null, Closure $scope = null);
/**
* Retrieve a prop within the current scope using "dot" notation.
*
* @param string|null $key
* @return mixed
*/
abstract protected function prop(string $key = null);
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Support\Collection;
class LoggedExceptionCollection extends Collection
{
//
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Support\Str;
use Symfony\Component\Console\Output\ConsoleOutput;
class ParallelConsoleOutput extends ConsoleOutput
{
/**
* The original output instance.
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* The output that should be ignored.
*
* @var array
*/
protected $ignore = [
'Running phpunit in',
'Configuration read from',
];
/**
* Create a new Parallel ConsoleOutput instance.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct($output)
{
parent::__construct(
$output->getVerbosity(),
$output->isDecorated(),
$output->getFormatter(),
);
$this->output = $output;
}
/**
* Writes a message to the output.
*
* @param string|iterable $messages
* @param bool $newline
* @param int $options
* @return void
*/
public function write($messages, bool $newline = false, int $options = 0)
{
$messages = collect($messages)->filter(function ($message) {
return ! Str::contains($message, $this->ignore);
});
$this->output->write($messages->toArray(), $newline, $options);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\ParallelTesting;
use ParaTest\Runners\PHPUnit\Options;
use ParaTest\Runners\PHPUnit\RunnerInterface;
use ParaTest\Runners\PHPUnit\WrapperRunner;
use PHPUnit\TextUI\XmlConfiguration\PhpHandler;
use RuntimeException;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
class ParallelRunner implements RunnerInterface
{
/**
* The application resolver callback.
*
* @var \Closure|null
*/
protected static $applicationResolver;
/**
* The runner resolver callback.
*
* @var \Closure|null
*/
protected static $runnerResolver;
/**
* The original test runner options.
*
* @var \ParaTest\Runners\PHPUnit\Options
*/
protected $options;
/**
* The output instance.
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* The original test runner.
*
* @var \ParaTest\Runners\PHPUnit\RunnerInterface
*/
protected $runner;
/**
* Creates a new test runner instance.
*
* @param \ParaTest\Runners\PHPUnit\Options $options
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct(Options $options, OutputInterface $output)
{
$this->options = $options;
if ($output instanceof ConsoleOutput) {
$output = new ParallelConsoleOutput($output);
}
$runnerResolver = static::$runnerResolver ?: function (Options $options, OutputInterface $output) {
return new WrapperRunner($options, $output);
};
$this->runner = $runnerResolver($options, $output);
}
/**
* Set the application resolver callback.
*
* @param \Closure|null $resolver
* @return void
*/
public static function resolveApplicationUsing($resolver)
{
static::$applicationResolver = $resolver;
}
/**
* Set the runner resolver callback.
*
* @param \Closure|null $resolver
* @return void
*/
public static function resolveRunnerUsing($resolver)
{
static::$runnerResolver = $resolver;
}
/**
* Runs the test suite.
*
* @return void
*/
public function run(): void
{
(new PhpHandler)->handle($this->options->configuration()->php());
$this->forEachProcess(function () {
ParallelTesting::callSetUpProcessCallbacks();
});
try {
$this->runner->run();
} finally {
$this->forEachProcess(function () {
ParallelTesting::callTearDownProcessCallbacks();
});
}
}
/**
* Returns the highest exit code encountered throughout the course of test execution.
*
* @return int
*/
public function getExitCode(): int
{
return $this->runner->getExitCode();
}
/**
* Apply the given callback for each process.
*
* @param callable $callback
* @return void
*/
protected function forEachProcess($callback)
{
collect(range(1, $this->options->processes()))->each(function ($token) use ($callback) {
tap($this->createApplication(), function ($app) use ($callback, $token) {
ParallelTesting::resolveTokenUsing(fn () => $token);
$callback($app);
})->flush();
});
}
/**
* Creates the application.
*
* @return \Illuminate\Contracts\Foundation\Application
*
* @throws \RuntimeException
*/
protected function createApplication()
{
$applicationResolver = static::$applicationResolver ?: function () {
if (trait_exists(\Tests\CreatesApplication::class)) {
$applicationCreator = new class
{
use \Tests\CreatesApplication;
};
return $applicationCreator->createApplication();
} elseif (file_exists(getcwd().'/bootstrap/app.php')) {
$app = require getcwd().'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
throw new RuntimeException('Parallel Runner unable to resolve application.');
};
return $applicationResolver();
}
}

View File

@@ -0,0 +1,291 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Str;
class ParallelTesting
{
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The options resolver callback.
*
* @var \Closure|null
*/
protected $optionsResolver;
/**
* The token resolver callback.
*
* @var \Closure|null
*/
protected $tokenResolver;
/**
* All of the registered "setUp" process callbacks.
*
* @var array
*/
protected $setUpProcessCallbacks = [];
/**
* All of the registered "setUp" test case callbacks.
*
* @var array
*/
protected $setUpTestCaseCallbacks = [];
/**
* All of the registered "setUp" test database callbacks.
*
* @var array
*/
protected $setUpTestDatabaseCallbacks = [];
/**
* All of the registered "tearDown" process callbacks.
*
* @var array
*/
protected $tearDownProcessCallbacks = [];
/**
* All of the registered "tearDown" test case callbacks.
*
* @var array
*/
protected $tearDownTestCaseCallbacks = [];
/**
* Create a new parallel testing instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Set a callback that should be used when resolving options.
*
* @param \Closure|null $resolver
* @return void
*/
public function resolveOptionsUsing($resolver)
{
$this->optionsResolver = $resolver;
}
/**
* Set a callback that should be used when resolving the unique process token.
*
* @param \Closure|null $resolver
* @return void
*/
public function resolveTokenUsing($resolver)
{
$this->tokenResolver = $resolver;
}
/**
* Register a "setUp" process callback.
*
* @param callable $callback
* @return void
*/
public function setUpProcess($callback)
{
$this->setUpProcessCallbacks[] = $callback;
}
/**
* Register a "setUp" test case callback.
*
* @param callable $callback
* @return void
*/
public function setUpTestCase($callback)
{
$this->setUpTestCaseCallbacks[] = $callback;
}
/**
* Register a "setUp" test database callback.
*
* @param callable $callback
* @return void
*/
public function setUpTestDatabase($callback)
{
$this->setUpTestDatabaseCallbacks[] = $callback;
}
/**
* Register a "tearDown" process callback.
*
* @param callable $callback
* @return void
*/
public function tearDownProcess($callback)
{
$this->tearDownProcessCallbacks[] = $callback;
}
/**
* Register a "tearDown" test case callback.
*
* @param callable $callback
* @return void
*/
public function tearDownTestCase($callback)
{
$this->tearDownTestCaseCallbacks[] = $callback;
}
/**
* Call all of the "setUp" process callbacks.
*
* @return void
*/
public function callSetUpProcessCallbacks()
{
$this->whenRunningInParallel(function () {
foreach ($this->setUpProcessCallbacks as $callback) {
$this->container->call($callback, [
'token' => $this->token(),
]);
}
});
}
/**
* Call all of the "setUp" test case callbacks.
*
* @param \Illuminate\Foundation\Testing\TestCase $testCase
* @return void
*/
public function callSetUpTestCaseCallbacks($testCase)
{
$this->whenRunningInParallel(function () use ($testCase) {
foreach ($this->setUpTestCaseCallbacks as $callback) {
$this->container->call($callback, [
'testCase' => $testCase,
'token' => $this->token(),
]);
}
});
}
/**
* Call all of the "setUp" test database callbacks.
*
* @param string $database
* @return void
*/
public function callSetUpTestDatabaseCallbacks($database)
{
$this->whenRunningInParallel(function () use ($database) {
foreach ($this->setUpTestDatabaseCallbacks as $callback) {
$this->container->call($callback, [
'database' => $database,
'token' => $this->token(),
]);
}
});
}
/**
* Call all of the "tearDown" process callbacks.
*
* @return void
*/
public function callTearDownProcessCallbacks()
{
$this->whenRunningInParallel(function () {
foreach ($this->tearDownProcessCallbacks as $callback) {
$this->container->call($callback, [
'token' => $this->token(),
]);
}
});
}
/**
* Call all of the "tearDown" test case callbacks.
*
* @param \Illuminate\Foundation\Testing\TestCase $testCase
* @return void
*/
public function callTearDownTestCaseCallbacks($testCase)
{
$this->whenRunningInParallel(function () use ($testCase) {
foreach ($this->tearDownTestCaseCallbacks as $callback) {
$this->container->call($callback, [
'testCase' => $testCase,
'token' => $this->token(),
]);
}
});
}
/**
* Get a parallel testing option.
*
* @param string $option
* @return mixed
*/
public function option($option)
{
$optionsResolver = $this->optionsResolver ?: function ($option) {
$option = 'LARAVEL_PARALLEL_TESTING_'.Str::upper($option);
return $_SERVER[$option] ?? false;
};
return $optionsResolver($option);
}
/**
* Gets a unique test token.
*
* @return string|false
*/
public function token()
{
return $this->tokenResolver
? call_user_func($this->tokenResolver)
: ($_SERVER['TEST_TOKEN'] ?? false);
}
/**
* Apply the callback if tests are running in parallel.
*
* @param callable $callback
* @return void
*/
protected function whenRunningInParallel($callback)
{
if ($this->inParallel()) {
$callback();
}
}
/**
* Indicates if the current tests are been run in parallel.
*
* @return bool
*/
protected function inParallel()
{
return ! empty($_SERVER['LARAVEL_PARALLEL_TESTING']) && $this->token();
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
use Illuminate\Testing\Concerns\TestDatabases;
class ParallelTestingServiceProvider extends ServiceProvider implements DeferrableProvider
{
use TestDatabases;
/**
* Boot the application's service providers.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->bootTestDatabase();
}
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
if ($this->app->runningInConsole()) {
$this->app->singleton(ParallelTesting::class, function () {
return new ParallelTesting($this->app);
});
}
}
}

View File

@@ -0,0 +1,483 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Mockery;
use Mockery\Exception\NoMatchingExpectationException;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
class PendingCommand
{
/**
* The test being run.
*
* @var \Illuminate\Foundation\Testing\TestCase
*/
public $test;
/**
* The application instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $app;
/**
* The command to run.
*
* @var string
*/
protected $command;
/**
* The parameters to pass to the command.
*
* @var array
*/
protected $parameters;
/**
* The expected exit code.
*
* @var int
*/
protected $expectedExitCode;
/**
* The unexpected exit code.
*
* @var int
*/
protected $unexpectedExitCode;
/**
* Determine if the command has executed.
*
* @var bool
*/
protected $hasExecuted = false;
/**
* Create a new pending console command run.
*
* @param \PHPUnit\Framework\TestCase $test
* @param \Illuminate\Contracts\Container\Container $app
* @param string $command
* @param array $parameters
* @return void
*/
public function __construct(PHPUnitTestCase $test, Container $app, $command, $parameters)
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
/**
* Specify an expected question that will be asked when the command runs.
*
* @param string $question
* @param string|bool $answer
* @return $this
*/
public function expectsQuestion($question, $answer)
{
$this->test->expectedQuestions[] = [$question, $answer];
return $this;
}
/**
* Specify an expected confirmation question that will be asked when the command runs.
*
* @param string $question
* @param string $answer
* @return $this
*/
public function expectsConfirmation($question, $answer = 'no')
{
return $this->expectsQuestion($question, strtolower($answer) === 'yes');
}
/**
* Specify an expected choice question with expected answers that will be asked/shown when the command runs.
*
* @param string $question
* @param string|array $answer
* @param array $answers
* @param bool $strict
* @return $this
*/
public function expectsChoice($question, $answer, $answers, $strict = false)
{
$this->test->expectedChoices[$question] = [
'expected' => $answers,
'strict' => $strict,
];
return $this->expectsQuestion($question, $answer);
}
/**
* Specify output that should be printed when the command runs.
*
* @param string $output
* @return $this
*/
public function expectsOutput($output)
{
$this->test->expectedOutput[] = $output;
return $this;
}
/**
* Specify output that should never be printed when the command runs.
*
* @param string $output
* @return $this
*/
public function doesntExpectOutput($output)
{
$this->test->unexpectedOutput[$output] = false;
return $this;
}
/**
* Specify that the given string should be contained in the command output.
*
* @param string $string
* @return $this
*/
public function expectsOutputToContain($string)
{
$this->test->expectedOutputSubstrings[] = $string;
return $this;
}
/**
* Specify that the given string shouldn't be contained in the command output.
*
* @param string $string
* @return $this
*/
public function doesntExpectOutputToContain($string)
{
$this->test->unexpectedOutputSubstrings[$string] = false;
return $this;
}
/**
* Specify a table that should be printed when the command runs.
*
* @param array $headers
* @param \Illuminate\Contracts\Support\Arrayable|array $rows
* @param string $tableStyle
* @param array $columnStyles
* @return $this
*/
public function expectsTable($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
{
$table = (new Table($output = new BufferedOutput))
->setHeaders((array) $headers)
->setRows($rows instanceof Arrayable ? $rows->toArray() : $rows)
->setStyle($tableStyle);
foreach ($columnStyles as $columnIndex => $columnStyle) {
$table->setColumnStyle($columnIndex, $columnStyle);
}
$table->render();
$lines = array_filter(
explode(PHP_EOL, $output->fetch())
);
foreach ($lines as $line) {
$this->expectsOutput($line);
}
return $this;
}
/**
* Assert that the command has the given exit code.
*
* @param int $exitCode
* @return $this
*/
public function assertExitCode($exitCode)
{
$this->expectedExitCode = $exitCode;
return $this;
}
/**
* Assert that the command does not have the given exit code.
*
* @param int $exitCode
* @return $this
*/
public function assertNotExitCode($exitCode)
{
$this->unexpectedExitCode = $exitCode;
return $this;
}
/**
* Assert that the command has the success exit code.
*
* @return $this
*/
public function assertSuccessful()
{
return $this->assertExitCode(Command::SUCCESS);
}
/**
* Assert that the command has the success exit code.
*
* @return $this
*/
public function assertOk()
{
return $this->assertSuccessful();
}
/**
* Assert that the command does not have the success exit code.
*
* @return $this
*/
public function assertFailed()
{
return $this->assertNotExitCode(Command::SUCCESS);
}
/**
* Execute the command.
*
* @return int
*/
public function execute()
{
return $this->run();
}
/**
* Execute the command.
*
* @return int
*
* @throws \Mockery\Exception\NoMatchingExpectationException
*/
public function run()
{
$this->hasExecuted = true;
$mock = $this->mockConsoleOutput();
try {
$exitCode = $this->app->make(Kernel::class)->call($this->command, $this->parameters, $mock);
} catch (NoMatchingExpectationException $e) {
if ($e->getMethodName() === 'askQuestion') {
$this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
}
throw $e;
}
if ($this->expectedExitCode !== null) {
$this->test->assertEquals(
$this->expectedExitCode, $exitCode,
"Expected status code {$this->expectedExitCode} but received {$exitCode}."
);
} elseif (! is_null($this->unexpectedExitCode)) {
$this->test->assertNotEquals(
$this->unexpectedExitCode, $exitCode,
"Unexpected status code {$this->unexpectedExitCode} was received."
);
}
$this->verifyExpectations();
$this->flushExpectations();
return $exitCode;
}
/**
* Determine if expected questions / choices / outputs are fulfilled.
*
* @return void
*/
protected function verifyExpectations()
{
if (count($this->test->expectedQuestions)) {
$this->test->fail('Question "'.Arr::first($this->test->expectedQuestions)[0].'" was not asked.');
}
if (count($this->test->expectedChoices) > 0) {
foreach ($this->test->expectedChoices as $question => $answers) {
$assertion = $answers['strict'] ? 'assertEquals' : 'assertEqualsCanonicalizing';
$this->test->{$assertion}(
$answers['expected'],
$answers['actual'],
'Question "'.$question.'" has different options.'
);
}
}
if (count($this->test->expectedOutput)) {
$this->test->fail('Output "'.Arr::first($this->test->expectedOutput).'" was not printed.');
}
if (count($this->test->expectedOutputSubstrings)) {
$this->test->fail('Output does not contain "'.Arr::first($this->test->expectedOutputSubstrings).'".');
}
if ($output = array_search(true, $this->test->unexpectedOutput)) {
$this->test->fail('Output "'.$output.'" was printed.');
}
if ($output = array_search(true, $this->test->unexpectedOutputSubstrings)) {
$this->test->fail('Output "'.$output.'" was printed.');
}
}
/**
* Mock the application's console output.
*
* @return \Mockery\MockInterface
*/
protected function mockConsoleOutput()
{
$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
new ArrayInput($this->parameters), $this->createABufferedOutputMock(),
]);
foreach ($this->test->expectedQuestions as $i => $question) {
$mock->shouldReceive('askQuestion')
->once()
->ordered()
->with(Mockery::on(function ($argument) use ($question) {
if (isset($this->test->expectedChoices[$question[0]])) {
$this->test->expectedChoices[$question[0]]['actual'] = $argument->getAutocompleterValues();
}
return $argument->getQuestion() == $question[0];
}))
->andReturnUsing(function () use ($question, $i) {
unset($this->test->expectedQuestions[$i]);
return $question[1];
});
}
$this->app->bind(OutputStyle::class, function () use ($mock) {
return $mock;
});
return $mock;
}
/**
* Create a mock for the buffered output.
*
* @return \Mockery\MockInterface
*/
private function createABufferedOutputMock()
{
$mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
->shouldAllowMockingProtectedMethods()
->shouldIgnoreMissing();
foreach ($this->test->expectedOutput as $i => $output) {
$mock->shouldReceive('doWrite')
->once()
->ordered()
->with($output, Mockery::any())
->andReturnUsing(function () use ($i) {
unset($this->test->expectedOutput[$i]);
});
}
foreach ($this->test->expectedOutputSubstrings as $i => $text) {
$mock->shouldReceive('doWrite')
->atLeast()
->times(0)
->withArgs(fn ($output) => str_contains($output, $text))
->andReturnUsing(function () use ($i) {
unset($this->test->expectedOutputSubstrings[$i]);
});
}
foreach ($this->test->unexpectedOutput as $output => $displayed) {
$mock->shouldReceive('doWrite')
->atLeast()
->times(0)
->ordered()
->with($output, Mockery::any())
->andReturnUsing(function () use ($output) {
$this->test->unexpectedOutput[$output] = true;
});
}
foreach ($this->test->unexpectedOutputSubstrings as $text => $displayed) {
$mock->shouldReceive('doWrite')
->atLeast()
->times(0)
->withArgs(fn ($output) => str_contains($output, $text))
->andReturnUsing(function () use ($text) {
$this->test->unexpectedOutputSubstrings[$text] = true;
});
}
return $mock;
}
/**
* Flush the expectations from the test case.
*
* @return void
*/
protected function flushExpectations()
{
$this->test->expectedOutput = [];
$this->test->expectedOutputSubstrings = [];
$this->test->unexpectedOutput = [];
$this->test->unexpectedOutputSubstrings = [];
$this->test->expectedTables = [];
$this->test->expectedQuestions = [];
$this->test->expectedChoices = [];
}
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Illuminate\Testing;
use Illuminate\Testing\Assert as PHPUnit;
use Illuminate\Testing\Constraints\SeeInOrder;
class TestComponent
{
/**
* The original component.
*
* @var \Illuminate\View\Component
*/
public $component;
/**
* The rendered component contents.
*
* @var string
*/
protected $rendered;
/**
* Create a new test component instance.
*
* @param \Illuminate\View\Component $component
* @param \Illuminate\View\View $view
* @return void
*/
public function __construct($component, $view)
{
$this->component = $component;
$this->rendered = $view->render();
}
/**
* Assert that the given string is contained within the rendered component.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertSee($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringContainsString((string) $value, $this->rendered);
return $this;
}
/**
* Assert that the given strings are contained in order within the rendered component.
*
* @param array $values
* @param bool $escape
* @return $this
*/
public function assertSeeInOrder(array $values, $escape = true)
{
$values = $escape ? array_map('e', $values) : $values;
PHPUnit::assertThat($values, new SeeInOrder($this->rendered));
return $this;
}
/**
* Assert that the given string is contained within the rendered component text.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertSeeText($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringContainsString((string) $value, strip_tags($this->rendered));
return $this;
}
/**
* Assert that the given strings are contained in order within the rendered component text.
*
* @param array $values
* @param bool $escape
* @return $this
*/
public function assertSeeTextInOrder(array $values, $escape = true)
{
$values = $escape ? array_map('e', $values) : $values;
PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->rendered)));
return $this;
}
/**
* Assert that the given string is not contained within the rendered component.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertDontSee($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringNotContainsString((string) $value, $this->rendered);
return $this;
}
/**
* Assert that the given string is not contained within the rendered component text.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertDontSeeText($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringNotContainsString((string) $value, strip_tags($this->rendered));
return $this;
}
/**
* Get the string contents of the rendered component.
*
* @return string
*/
public function __toString()
{
return $this->rendered;
}
/**
* Dynamically access properties on the underlying component.
*
* @param string $attribute
* @return mixed
*/
public function __get($attribute)
{
return $this->component->{$attribute};
}
/**
* Dynamically call methods on the underlying component.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->component->{$method}(...$parameters);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
<?php
namespace Illuminate\Testing;
use Closure;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Testing\Assert as PHPUnit;
use Illuminate\Testing\Constraints\SeeInOrder;
use Illuminate\View\View;
class TestView
{
use Macroable;
/**
* The original view.
*
* @var \Illuminate\View\View
*/
protected $view;
/**
* The rendered view contents.
*
* @var string
*/
protected $rendered;
/**
* Create a new test view instance.
*
* @param \Illuminate\View\View $view
* @return void
*/
public function __construct(View $view)
{
$this->view = $view;
$this->rendered = $view->render();
}
/**
* Assert that the response view has a given piece of bound data.
*
* @param string|array $key
* @param mixed $value
* @return $this
*/
public function assertViewHas($key, $value = null)
{
if (is_array($key)) {
return $this->assertViewHasAll($key);
}
if (is_null($value)) {
PHPUnit::assertTrue(Arr::has($this->view->gatherData(), $key));
} elseif ($value instanceof Closure) {
PHPUnit::assertTrue($value(Arr::get($this->view->gatherData(), $key)));
} elseif ($value instanceof Model) {
PHPUnit::assertTrue($value->is(Arr::get($this->view->gatherData(), $key)));
} elseif ($value instanceof Collection) {
$actual = Arr::get($this->view->gatherData(), $key);
PHPUnit::assertInstanceOf(Collection::class, $actual);
PHPUnit::assertSameSize($value, $actual);
$value->each(fn ($item, $index) => PHPUnit::assertTrue($actual->get($index)->is($item)));
} else {
PHPUnit::assertEquals($value, Arr::get($this->view->gatherData(), $key));
}
return $this;
}
/**
* Assert that the response view has a given list of bound data.
*
* @param array $bindings
* @return $this
*/
public function assertViewHasAll(array $bindings)
{
foreach ($bindings as $key => $value) {
if (is_int($key)) {
$this->assertViewHas($value);
} else {
$this->assertViewHas($key, $value);
}
}
return $this;
}
/**
* Assert that the response view is missing a piece of bound data.
*
* @param string $key
* @return $this
*/
public function assertViewMissing($key)
{
PHPUnit::assertFalse(Arr::has($this->view->gatherData(), $key));
return $this;
}
/**
* Assert that the given string is contained within the view.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertSee($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringContainsString((string) $value, $this->rendered);
return $this;
}
/**
* Assert that the given strings are contained in order within the view.
*
* @param array $values
* @param bool $escape
* @return $this
*/
public function assertSeeInOrder(array $values, $escape = true)
{
$values = $escape ? array_map('e', $values) : $values;
PHPUnit::assertThat($values, new SeeInOrder($this->rendered));
return $this;
}
/**
* Assert that the given string is contained within the view text.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertSeeText($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringContainsString((string) $value, strip_tags($this->rendered));
return $this;
}
/**
* Assert that the given strings are contained in order within the view text.
*
* @param array $values
* @param bool $escape
* @return $this
*/
public function assertSeeTextInOrder(array $values, $escape = true)
{
$values = $escape ? array_map('e', $values) : $values;
PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->rendered)));
return $this;
}
/**
* Assert that the given string is not contained within the view.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertDontSee($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringNotContainsString((string) $value, $this->rendered);
return $this;
}
/**
* Assert that the given string is not contained within the view text.
*
* @param string $value
* @param bool $escape
* @return $this
*/
public function assertDontSeeText($value, $escape = true)
{
$value = $escape ? e($value) : $value;
PHPUnit::assertStringNotContainsString((string) $value, strip_tags($this->rendered));
return $this;
}
/**
* Get the string contents of the rendered view.
*
* @return string
*/
public function __toString()
{
return $this->rendered;
}
}

View File

@@ -0,0 +1,45 @@
{
"name": "illuminate/testing",
"description": "The Illuminate Testing package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.0.2",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0",
"illuminate/support": "^9.0"
},
"autoload": {
"psr-4": {
"Illuminate\\Testing\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"suggest": {
"brianium/paratest": "Required to run tests in parallel (^6.0).",
"illuminate/console": "Required to assert console commands (^9.0).",
"illuminate/database": "Required to assert databases (^9.0).",
"illuminate/http": "Required to assert responses (^9.0).",
"mockery/mockery": "Required to use mocking (^1.5.1).",
"phpunit/phpunit": "Required to use assertions and run tests (^9.5.8)."
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}