final 1.2.3 - file sh per generare zip
This commit is contained in:
-21
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"name": "laravel/agent-detector",
|
||||
"description": "Detect if code is running in an AI agent or automated development environment",
|
||||
"keywords": [
|
||||
"php",
|
||||
"ai",
|
||||
"agent",
|
||||
"detection",
|
||||
"cursor",
|
||||
"claude",
|
||||
"devin",
|
||||
"automation"
|
||||
],
|
||||
"homepage": "https://github.com/laravel/agent-detector",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/agent-detector/issues",
|
||||
"source": "https://github.com/laravel/agent-detector"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.24.0",
|
||||
"pestphp/pest": "^3.8.5|^4.1.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2",
|
||||
"phpstan/phpstan": "^2.1.26",
|
||||
"rector/rector": "^2.1.7",
|
||||
"symfony/var-dumper": "^7.3.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\AgentDetector\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
},
|
||||
"files": [
|
||||
"tests/Overrides.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"preferred-install": "dist",
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "pint",
|
||||
"refactor": "rector",
|
||||
"test:type-coverage": "pest --type-coverage --exactly=100",
|
||||
"test:lint": "pint --test",
|
||||
"test:unit": "pest",
|
||||
"test:types": "phpstan",
|
||||
"test:refactor": "rector --dry-run",
|
||||
"test": [
|
||||
"@test:lint",
|
||||
"@test:type-coverage",
|
||||
"@test:unit",
|
||||
"@test:types",
|
||||
"@test:refactor"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\AgentDetector;
|
||||
|
||||
class AgentDetector
|
||||
{
|
||||
public const AGENT_ENV_VARS = [
|
||||
'CURSOR_AGENT' => KnownAgent::Cursor,
|
||||
'GEMINI_CLI' => KnownAgent::Gemini,
|
||||
'CODEX_SANDBOX' => KnownAgent::Codex,
|
||||
'CODEX_CI' => KnownAgent::Codex,
|
||||
'CODEX_THREAD_ID' => KnownAgent::Codex,
|
||||
'AUGMENT_AGENT' => KnownAgent::AugmentCli,
|
||||
'OPENCODE_CLIENT' => KnownAgent::Opencode,
|
||||
'OPENCODE' => KnownAgent::Opencode,
|
||||
'AMP_CURRENT_THREAD_ID' => KnownAgent::Amp,
|
||||
'CLAUDECODE' => KnownAgent::Claude,
|
||||
'CLAUDE_CODE' => KnownAgent::Claude,
|
||||
'REPL_ID' => KnownAgent::Replit,
|
||||
'COPILOT_MODEL' => KnownAgent::Copilot,
|
||||
'COPILOT_ALLOW_ALL' => KnownAgent::Copilot,
|
||||
'COPILOT_GITHUB_TOKEN' => KnownAgent::Copilot,
|
||||
'COPILOT_CLI' => KnownAgent::Copilot,
|
||||
'ANTIGRAVITY_AGENT' => KnownAgent::Antigravity,
|
||||
'PI_CODING_AGENT' => KnownAgent::Pi,
|
||||
'KIRO_AGENT_PATH' => KnownAgent::KiroCli,
|
||||
];
|
||||
|
||||
public static function detect(): AgentResult
|
||||
{
|
||||
return self::fromAiAgentEnvVar()
|
||||
?? self::fromKnownEnvVars()
|
||||
?? self::fromFileSystem()
|
||||
?? AgentResult::noAgent();
|
||||
}
|
||||
|
||||
protected static function fromAiAgentEnvVar(): ?AgentResult
|
||||
{
|
||||
$aiAgent = getenv('AI_AGENT');
|
||||
|
||||
if ($aiAgent === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$aiAgent = trim($aiAgent);
|
||||
|
||||
if ($aiAgent === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AgentResult::forAgent(match (true) {
|
||||
in_array($aiAgent, ['github-copilot', 'github-copilot-cli']) => KnownAgent::Copilot,
|
||||
str_starts_with($aiAgent, 'claude-code') => KnownAgent::Claude,
|
||||
default => $aiAgent,
|
||||
});
|
||||
}
|
||||
|
||||
protected static function fromKnownEnvVars(): ?AgentResult
|
||||
{
|
||||
foreach (self::AGENT_ENV_VARS as $envVar => $agent) {
|
||||
if (getenv($envVar) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return AgentResult::forAgent(match ($agent) {
|
||||
KnownAgent::Claude => getenv('CLAUDE_CODE_IS_COWORK') !== false ? KnownAgent::Cowork : KnownAgent::Claude,
|
||||
default => $agent,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static function fromFileSystem(): ?AgentResult
|
||||
{
|
||||
if (file_exists('/opt/.devin')) {
|
||||
return AgentResult::forAgent(KnownAgent::Devin);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\AgentDetector;
|
||||
|
||||
class AgentResult
|
||||
{
|
||||
public readonly bool $isAgent;
|
||||
|
||||
public function __construct(public readonly ?string $name = null)
|
||||
{
|
||||
$this->isAgent = $name !== null;
|
||||
}
|
||||
|
||||
public static function forAgent(KnownAgent|string $name): self
|
||||
{
|
||||
return new self($name instanceof KnownAgent ? $name->value : $name);
|
||||
}
|
||||
|
||||
public static function noAgent(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function knownAgent(): ?KnownAgent
|
||||
{
|
||||
return KnownAgent::tryFrom($this->name ?? '');
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\AgentDetector;
|
||||
|
||||
enum KnownAgent: string
|
||||
{
|
||||
case Cursor = 'cursor';
|
||||
case Claude = 'claude';
|
||||
case Cowork = 'cowork';
|
||||
case Devin = 'devin';
|
||||
case Replit = 'replit';
|
||||
case Gemini = 'gemini';
|
||||
case Codex = 'codex';
|
||||
case V0 = 'v0';
|
||||
case AugmentCli = 'augment-cli';
|
||||
case Opencode = 'opencode';
|
||||
case Amp = 'amp';
|
||||
case Copilot = 'copilot';
|
||||
case Antigravity = 'antigravity';
|
||||
case Pi = 'pi';
|
||||
case KiroCli = 'kiro-cli';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::AugmentCli => 'Augment CLI',
|
||||
self::KiroCli => 'Kiro CLI',
|
||||
self::V0 => 'v0',
|
||||
default => ucfirst($this->value),
|
||||
};
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\AgentDetector;
|
||||
|
||||
function detectAgent(): AgentResult
|
||||
{
|
||||
return AgentDetector::detect();
|
||||
}
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
Vendored
-70
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"description": "Easily delve into your Laravel application's log files directly from the command line.",
|
||||
"keywords": ["php", "tail", "laravel", "logs", "dev"],
|
||||
"homepage": "https://github.com/laravel/pail",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pail/issues",
|
||||
"source": "https://github.com/laravel/pail"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
},
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-mbstring": "*",
|
||||
"illuminate/console": "^10.24|^11.0|^12.0|^13.0",
|
||||
"illuminate/contracts": "^10.24|^11.0|^12.0|^13.0",
|
||||
"illuminate/log": "^10.24|^11.0|^12.0|^13.0",
|
||||
"illuminate/process": "^10.24|^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^10.24|^11.0|^12.0|^13.0",
|
||||
"nunomaduro/termwind": "^1.15|^2.0",
|
||||
"symfony/console": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "^10.24|^11.0|^12.0|^13.0",
|
||||
"laravel/pint": "^1.13",
|
||||
"orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0",
|
||||
"pestphp/pest": "^2.20|^3.0|^4.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0",
|
||||
"phpstan/phpstan": "^1.12.27",
|
||||
"symfony/var-dumper": "^6.3|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.3|^7.0|^8.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Pail\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Pail\\PailServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Process\Exceptions\ProcessTimedOutException;
|
||||
use Laravel\Pail\File;
|
||||
use Laravel\Pail\Guards\EnsurePcntlIsAvailable;
|
||||
use Laravel\Pail\Options;
|
||||
use Laravel\Pail\ProcessFactory;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
|
||||
use function Termwind\render;
|
||||
use function Termwind\renderUsing;
|
||||
|
||||
#[AsCommand(name: 'pail')]
|
||||
class PailCommand extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected $signature = 'pail
|
||||
{--filter= : Filter the logs by the given value}
|
||||
{--message= : Filter the logs by the given message}
|
||||
{--level= : Filter the logs by the given level}
|
||||
{--auth= : Filter the logs by the given authenticated ID}
|
||||
{--user= : Filter the logs by the given authenticated ID (alias for --auth)}
|
||||
{--timeout=3600 : The maximum execution time in seconds}';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected $description = 'Tails the application logs';
|
||||
|
||||
/**
|
||||
* The file instance, if any.
|
||||
*/
|
||||
protected ?File $file = null;
|
||||
|
||||
/**
|
||||
* Handles the command execution.
|
||||
*/
|
||||
public function handle(ProcessFactory $processFactory): void
|
||||
{
|
||||
EnsurePcntlIsAvailable::check();
|
||||
|
||||
renderUsing($this->output);
|
||||
render(<<<'HTML'
|
||||
<div class="max-w-150 mx-2 mt-1 flex">
|
||||
<div>
|
||||
<span class="px-1 bg-blue uppercase text-white">INFO</span>
|
||||
<span class="flex-1">
|
||||
<span class="ml-1 ">Tailing application logs.</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="flex-1"></span>
|
||||
<span class="text-gray ml-1">
|
||||
<span class="text-gray">Press Ctrl+C to exit</span>
|
||||
</span>
|
||||
</div>
|
||||
HTML,
|
||||
);
|
||||
|
||||
render(<<<'HTML'
|
||||
<div class="max-w-150 mx-2 flex">
|
||||
<div>
|
||||
</div>
|
||||
<span class="flex-1"></span>
|
||||
<span class="text-gray ml-1">
|
||||
<span class="text-gray">Use -v|-vv to show more details</span>
|
||||
</span>
|
||||
</div>
|
||||
HTML,
|
||||
);
|
||||
|
||||
$this->file = new File(storage_path('pail/'.uniqid().'.pail'));
|
||||
$this->file->create();
|
||||
$this->trap([SIGINT, SIGTERM], fn () => $this->file->destroy());
|
||||
|
||||
$options = Options::fromCommand($this);
|
||||
|
||||
assert($this->file instanceof File);
|
||||
|
||||
try {
|
||||
$processFactory->run($this->file, $this->output, $this->laravel->basePath(), $options);
|
||||
} catch (ProcessSignaledException $e) {
|
||||
if (in_array($e->getSignal(), [SIGINT, SIGTERM], true)) {
|
||||
$this->newLine();
|
||||
}
|
||||
} catch (ProcessTimedOutException $e) {
|
||||
$this->components->info('Maximum execution time exceeded.');
|
||||
} finally {
|
||||
$this->file?->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the object destruction.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->file) {
|
||||
$this->file->destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Contracts;
|
||||
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
|
||||
interface Printer
|
||||
{
|
||||
/**
|
||||
* Prints the given message logged.
|
||||
*/
|
||||
public function print(MessageLogged $messageLogged): void;
|
||||
}
|
||||
Vendored
-100
@@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class File implements Stringable
|
||||
{
|
||||
/**
|
||||
* The time to live of the file.
|
||||
*/
|
||||
protected const TTL = 3600;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the file.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $file,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the file exists.
|
||||
*/
|
||||
public function create(): void
|
||||
{
|
||||
if (! $this->exists()) {
|
||||
$directory = dirname($this->file);
|
||||
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
|
||||
file_put_contents($directory.'/.gitignore', "*\n!.gitignore\n");
|
||||
}
|
||||
|
||||
touch($this->file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file exists.
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file.
|
||||
*/
|
||||
public function destroy(): void
|
||||
{
|
||||
if ($this->exists()) {
|
||||
unlink($this->file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a log message to the file.
|
||||
*
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
if ($this->isStale()) {
|
||||
$this->destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$loggerFactory = new LoggerFactory($this);
|
||||
|
||||
$logger = $loggerFactory->create();
|
||||
|
||||
$logger->log($level, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file as string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file is staled.
|
||||
*/
|
||||
protected function isStale(): bool
|
||||
{
|
||||
$modificationTime = @filemtime($this->file);
|
||||
|
||||
if ($modificationTime === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return time() - $modificationTime > static::TTL;
|
||||
}
|
||||
}
|
||||
Vendored
-30
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Files
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the files.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $path,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of files.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, File>
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
$files = glob($this->path.'/*.pail') ?: [];
|
||||
|
||||
return collect($files)
|
||||
->map(fn (string $file) => new File($file));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Guards;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class EnsurePcntlIsAvailable
|
||||
{
|
||||
/**
|
||||
* Checks if the pcntl extension is available.
|
||||
*/
|
||||
public static function check(): void
|
||||
{
|
||||
if (! function_exists('pcntl_fork')) {
|
||||
throw new RuntimeException('The [pcntl] extension is required to run Pail.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-130
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Log\Context\Repository as ContextRepository;
|
||||
use Illuminate\Log\Events\MessageLogged;
|
||||
use Illuminate\Queue\Events\JobExceptionOccurred;
|
||||
use Illuminate\Queue\Events\JobProcessing;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Log\LogLevel;
|
||||
use Throwable;
|
||||
|
||||
class Handler
|
||||
{
|
||||
/**
|
||||
* The last lifecycle captured event.
|
||||
*/
|
||||
protected CommandStarting|JobProcessing|JobExceptionOccurred|null $lastLifecycleEvent = null;
|
||||
|
||||
/**
|
||||
* The artisan command being executed, if any.
|
||||
*/
|
||||
protected ?string $artisanCommand = null;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the handler.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Container $container,
|
||||
protected Files $files,
|
||||
protected bool $runningInConsole,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the given message logged.
|
||||
*/
|
||||
public function log(MessageLogged $messageLogged): void
|
||||
{
|
||||
$files = $this->files->all();
|
||||
|
||||
if ($files->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$messageLogged->level === LogLevel::WARNING
|
||||
&& Str::contains($messageLogged->message, ['deprecated', 'Deprecated', '[\ReturnTypeWillChange]'])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context = $this->context($messageLogged);
|
||||
|
||||
$files->each(
|
||||
fn (File $file) => $file->log(
|
||||
$messageLogged->level,
|
||||
$messageLogged->message,
|
||||
$context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last application lifecycle event.
|
||||
*/
|
||||
public function setLastLifecycleEvent(CommandStarting|JobProcessing|JobExceptionOccurred|null $event): void
|
||||
{
|
||||
if ($event instanceof CommandStarting) {
|
||||
$this->artisanCommand = $event->command;
|
||||
}
|
||||
|
||||
$this->lastLifecycleEvent = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the context array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function context(MessageLogged $messageLogged): array
|
||||
{
|
||||
$context = ['__pail' => ['origin' => match (true) {
|
||||
$this->artisanCommand && $this->lastLifecycleEvent && in_array($this->lastLifecycleEvent::class, [JobProcessing::class, JobExceptionOccurred::class]) => [
|
||||
'type' => 'queue',
|
||||
'command' => $this->artisanCommand,
|
||||
'queue' => $this->lastLifecycleEvent->job->getQueue(),
|
||||
'job' => $this->lastLifecycleEvent->job->resolveName(),
|
||||
],
|
||||
$this->runningInConsole => [
|
||||
'type' => 'console',
|
||||
'command' => $this->artisanCommand,
|
||||
],
|
||||
default => [
|
||||
'type' => 'http',
|
||||
'method' => request()->method(),
|
||||
'path' => request()->path(),
|
||||
'auth_id' => Auth::id(),
|
||||
'auth_email' => Auth::user() instanceof User ? Auth::user()->email : null, // @phpstan-ignore property.notFound
|
||||
],
|
||||
}]];
|
||||
|
||||
if (isset($messageLogged->context['exception']) && $this->lastLifecycleEvent instanceof JobExceptionOccurred) {
|
||||
if ($messageLogged->context['exception'] === $this->lastLifecycleEvent->exception) {
|
||||
$this->setLastLifecycleEvent(null);
|
||||
}
|
||||
}
|
||||
|
||||
$context['__pail']['origin']['trace'] = isset($messageLogged->context['exception'])
|
||||
&& $messageLogged->context['exception'] instanceof Throwable ? collect($messageLogged->context['exception']->getTrace())
|
||||
->filter(fn (array $frame) => isset($frame['file']))
|
||||
->map(fn (array $frame) => [
|
||||
'file' => $frame['file'], // @phpstan-ignore offsetAccess.notFound
|
||||
'line' => $frame['line'] ?? null,
|
||||
])->values()
|
||||
: null;
|
||||
|
||||
return collect($messageLogged->context)
|
||||
->merge($context)
|
||||
->when($this->container->bound(ContextRepository::class), function (Collection $context) {
|
||||
return $context->merge($this->container->make(ContextRepository::class)->all());
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class LoggerFactory
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the logger factory.
|
||||
*/
|
||||
public function __construct(
|
||||
protected File $file,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the logger.
|
||||
*/
|
||||
public function create(): LoggerInterface
|
||||
{
|
||||
$handler = new StreamHandler($this->file->__toString(), Level::Debug);
|
||||
$handler->setFormatter(new JsonFormatter);
|
||||
|
||||
return new Logger('pail', [$handler]);
|
||||
}
|
||||
}
|
||||
Vendored
-76
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
|
||||
class Options
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the tail options.
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $timeout,
|
||||
protected ?string $authId,
|
||||
protected ?string $level,
|
||||
protected ?string $filter,
|
||||
protected ?string $message,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the tail options from the given console command.
|
||||
*/
|
||||
public static function fromCommand(Command $command): static
|
||||
{
|
||||
$authId = $command->option('auth') ?? $command->option('user');
|
||||
assert(is_string($authId) || $authId === null);
|
||||
|
||||
$level = $command->option('level');
|
||||
assert(is_string($level) || $level === null);
|
||||
|
||||
$filter = $command->option('filter');
|
||||
assert(is_string($filter) || $filter === null);
|
||||
|
||||
$message = $command->option('message');
|
||||
assert(is_string($message) || $message === null);
|
||||
|
||||
$timeout = (int) $command->option('timeout');
|
||||
|
||||
return new static($timeout, $authId, $level, $filter, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the tail options accept the given message logged.
|
||||
*/
|
||||
public function accepts(MessageLogged $messageLogged): bool
|
||||
{
|
||||
if (is_string($this->authId) && $messageLogged->authId() !== $this->authId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($this->level) && strtolower($messageLogged->level()) !== strtolower($this->level)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($this->filter) && ! str_contains(strtolower((string) $messageLogged), strtolower($this->filter))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($this->message) && ! str_contains(strtolower($messageLogged->message()), strtolower($this->message))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of seconds before the process is killed.
|
||||
*/
|
||||
public function timeout(): int
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Log\Events\MessageLogged;
|
||||
use Illuminate\Queue\Events\JobExceptionOccurred;
|
||||
use Illuminate\Queue\Events\JobProcessed;
|
||||
use Illuminate\Queue\Events\JobProcessing;
|
||||
use Illuminate\Support\Env;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Pail\Console\Commands\PailCommand;
|
||||
|
||||
class PailServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Registers the application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(
|
||||
Files::class,
|
||||
fn (Application $app) => new Files($app->storagePath('pail'))
|
||||
);
|
||||
|
||||
$this->app->singleton(Handler::class, fn (Application $app) => new Handler(
|
||||
$app,
|
||||
$app->make(Files::class),
|
||||
$app->runningInConsole(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstraps the application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if (! $this->runningPailTests() && ($this->app->runningUnitTests() || ($_ENV['VAPOR_SSM_PATH'] ?? false))) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Illuminate\Contracts\Events\Dispatcher $events */
|
||||
$events = $this->app->make('events');
|
||||
|
||||
$events->listen(MessageLogged::class, function (MessageLogged $messageLogged) {
|
||||
/** @var Handler $handler */
|
||||
$handler = $this->app->make(Handler::class);
|
||||
|
||||
$handler->log($messageLogged);
|
||||
});
|
||||
|
||||
$events->listen([CommandStarting::class, JobProcessing::class, JobExceptionOccurred::class], function (CommandStarting|JobProcessing|JobExceptionOccurred $lifecycleEvent) {
|
||||
/** @var Handler $handler */
|
||||
$handler = $this->app->make(Handler::class);
|
||||
|
||||
$handler->setLastLifecycleEvent($lifecycleEvent);
|
||||
});
|
||||
|
||||
$events->listen([JobProcessed::class], function () {
|
||||
/** @var Handler $handler */
|
||||
$handler = $this->app->make(Handler::class);
|
||||
|
||||
$handler->setLastLifecycleEvent(null);
|
||||
});
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
PailCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Pail's test suite is running.
|
||||
*/
|
||||
protected function runningPailTests(): bool
|
||||
{
|
||||
return (bool) (Env::get('PAIL_TESTS') ?? false);
|
||||
}
|
||||
}
|
||||
-283
@@ -1,283 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Printers;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Env;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Pail\Contracts\Printer;
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
use Laravel\Pail\ValueObjects\Origin\Http;
|
||||
use Laravel\Pail\ValueObjects\Origin\Queue;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Termwind\render;
|
||||
use function Termwind\renderUsing;
|
||||
use function Termwind\terminal;
|
||||
|
||||
class CliPrinter implements Printer
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function print(MessageLogged $messageLogged): void
|
||||
{
|
||||
$classOrType = $this->truncateClassOrType($messageLogged->classOrType());
|
||||
$color = $messageLogged->color();
|
||||
$message = $this->truncateMessage($messageLogged->message());
|
||||
$date = $this->output->isVerbose() ? $messageLogged->date() : $messageLogged->time();
|
||||
|
||||
$fileHtml = $this->fileHtml($messageLogged->file(), $classOrType);
|
||||
$messageHtml = $this->messageHtml($message);
|
||||
$optionsHtml = $this->optionsHtml($messageLogged);
|
||||
$traceHtml = $this->traceHtml($messageLogged);
|
||||
|
||||
$messageClasses = $this->output->isVerbose() ? '' : 'truncate';
|
||||
|
||||
$endingTopRight = $this->output->isVerbose() ? '' : '┐';
|
||||
$endingMiddle = $this->output->isVerbose() ? '' : '│';
|
||||
$endingBottomRight = $this->output->isVerbose() ? '' : '┘';
|
||||
|
||||
renderUsing($this->output);
|
||||
render(<<<HTML
|
||||
<div class="max-w-150">
|
||||
<div class="flex">
|
||||
<div>
|
||||
<span class="mr-1 text-gray">┌</span>
|
||||
<span class="text-gray">$date</span>
|
||||
<span class="px-1 text-$color font-bold">$classOrType</span>
|
||||
</div>
|
||||
<span class="flex-1 content-repeat-[─] text-gray"></span>
|
||||
<span class="text-gray">
|
||||
$fileHtml
|
||||
<span class="text-gray">$endingTopRight</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex $messageClasses">
|
||||
<span>
|
||||
<span class="mr-1 text-gray">│</span>
|
||||
$messageHtml
|
||||
</span>
|
||||
<span class="flex-1"></span>
|
||||
<span class="flex-1 text-gray text-right">$endingMiddle</span>
|
||||
</div>
|
||||
$traceHtml
|
||||
<div class="flex text-gray">
|
||||
<span>└</span>
|
||||
<span class="mr-1 flex-1 content-repeat-[─]"></span>
|
||||
$optionsHtml
|
||||
<span class="ml-1">$endingBottomRight</span>
|
||||
</div>
|
||||
</div>
|
||||
HTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance printer instance.
|
||||
*/
|
||||
public function __construct(protected OutputInterface $output, protected string $basePath)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file html.
|
||||
*/
|
||||
protected function fileHtml(?string $file, string $classOrType): ?string
|
||||
{
|
||||
if (is_null($file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Env::get('PAIL_TESTS') ?? false) {
|
||||
$file = $this->basePath.'/app/MyClass.php:12';
|
||||
}
|
||||
|
||||
$file = str_replace($this->basePath.'/', '', $file);
|
||||
|
||||
if (! $this->output->isVerbose()) {
|
||||
$file = Str::of($file)
|
||||
->explode('/')
|
||||
->when(
|
||||
fn (Collection $file) => $file->count() > 4,
|
||||
fn (Collection $file) => $file->take(2)->merge(
|
||||
['…', (string) $file->last()],
|
||||
),
|
||||
)->implode('/');
|
||||
|
||||
$fileSize = max(0, min(terminal()->width() - strlen($classOrType) - 16, 145));
|
||||
|
||||
if (strlen($file) > $fileSize) {
|
||||
$file = mb_substr($file, 0, $fileSize).'…';
|
||||
}
|
||||
}
|
||||
|
||||
if ($file === '…') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = str_replace('……', '…', $file);
|
||||
|
||||
return <<<HTML
|
||||
<span class="text-gray mx-1">
|
||||
$file
|
||||
</span>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the message html.
|
||||
*/
|
||||
protected function messageHtml(string $message): string
|
||||
{
|
||||
if (empty($message)) {
|
||||
return '<span class="text-gray">No message.</span>';
|
||||
}
|
||||
|
||||
$message = htmlspecialchars($message);
|
||||
|
||||
if (strstr($message, PHP_EOL)) {
|
||||
return "<pre>$message</pre>";
|
||||
}
|
||||
|
||||
return "<span>$message</span>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the class or type, if needed.
|
||||
*/
|
||||
protected function truncateClassOrType(string $classOrType): string
|
||||
{
|
||||
if ($this->output->isVerbose()) {
|
||||
return $classOrType;
|
||||
}
|
||||
|
||||
return Str::of($classOrType)
|
||||
->explode('\\')
|
||||
->when(
|
||||
fn (Collection $classOrType) => $classOrType->count() > 4,
|
||||
fn (Collection $classOrType) => $classOrType->take(2)->merge(
|
||||
['…', (string) $classOrType->last()]
|
||||
),
|
||||
)->implode('\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the message, if needed.
|
||||
*/
|
||||
protected function truncateMessage(string $message): string
|
||||
{
|
||||
if (! $this->output->isVerbose()) {
|
||||
$messageSize = max(0, min(terminal()->width() - 5, 145));
|
||||
|
||||
if (strlen($message) > $messageSize) {
|
||||
$message = mb_substr($message, 0, $messageSize).'…';
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the options html.
|
||||
*/
|
||||
public function optionsHtml(MessageLogged $messageLogged): string
|
||||
{
|
||||
$origin = $messageLogged->origin();
|
||||
|
||||
if ($origin instanceof Http) {
|
||||
if (str_starts_with($path = $origin->path, '/') === false) {
|
||||
$path = '/'.$origin->path;
|
||||
}
|
||||
|
||||
$options = [
|
||||
strtoupper($origin->method) => $path,
|
||||
'Auth ID' => $origin->authId
|
||||
? ($origin->authId.($origin->authEmail ? " ({$origin->authEmail})" : ''))
|
||||
: 'guest',
|
||||
];
|
||||
} elseif ($origin instanceof Queue) {
|
||||
$options = [
|
||||
$origin->command ? "artisan {$origin->command}" : null,
|
||||
$origin->queue,
|
||||
$origin->job,
|
||||
];
|
||||
} else {
|
||||
$options = [
|
||||
$origin->command ? "artisan {$origin->command}" : 'artisan',
|
||||
];
|
||||
}
|
||||
|
||||
return collect($options)->merge(
|
||||
$messageLogged->context()
|
||||
)->reject(fn (mixed $value, string|int $key) => is_int($key) && is_null($value))
|
||||
->map(fn (mixed $value) => is_string($value) ? $value : var_export($value, true))
|
||||
->map(fn (string $value) => htmlspecialchars($value))
|
||||
->map(fn (string $value, string|int $key) => is_string($key) ? "$key: $value" : $value)
|
||||
->map(fn (string $value) => "<span class=\"font-bold\">$value</span>")
|
||||
->implode(' • ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trace html.
|
||||
*/
|
||||
public function traceHtml(MessageLogged $messageLogged): string
|
||||
{
|
||||
if (! $this->output->isVeryVerbose()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$trace = $messageLogged->trace();
|
||||
|
||||
if (Env::get('PAIL_TESTS') ?? false) {
|
||||
$trace = [
|
||||
[
|
||||
'line' => 12,
|
||||
'file' => $this->basePath.'/app/MyClass.php',
|
||||
],
|
||||
[
|
||||
'line' => 34,
|
||||
'file' => $this->basePath.'/app/MyClass.php',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (is_null($trace)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return collect($trace)
|
||||
->map(function (array $frame, int $index) {
|
||||
$number = $index + 1;
|
||||
|
||||
[
|
||||
'line' => $line,
|
||||
'file' => $file,
|
||||
] = $frame;
|
||||
|
||||
$file = str_replace($this->basePath.'/', '', $file);
|
||||
|
||||
$remainingTraces = '';
|
||||
|
||||
if (! $this->output->isVerbose()) {
|
||||
$file = (string) Str::of($file)
|
||||
->explode('/')
|
||||
->when(
|
||||
fn (Collection $file) => $file->count() > 4,
|
||||
fn (Collection $file) => $file->take(2)->merge(
|
||||
['…', (string) $file->last()],
|
||||
),
|
||||
)->implode('/');
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex text-gray">
|
||||
<span>
|
||||
<span class="mr-1 text-gray">│</span>
|
||||
<span>$number. $file:$line $remainingTraces</span>
|
||||
</span>
|
||||
</div>
|
||||
HTML;
|
||||
})->implode('');
|
||||
}
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Pail\Printers\CliPrinter;
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ProcessFactory
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the process factory.
|
||||
*/
|
||||
public function run(File $file, OutputInterface $output, string $basePath, Options $options): void
|
||||
{
|
||||
$printer = new CliPrinter($output, $basePath);
|
||||
|
||||
$remainingBuffer = '';
|
||||
|
||||
Process::timeout($options->timeout())
|
||||
->tty(false)
|
||||
->run(
|
||||
$this->command($file),
|
||||
function (string $type, string $buffer) use ($options, $printer, &$remainingBuffer) {
|
||||
$lines = Str::of($buffer)->explode("\n");
|
||||
|
||||
if ($remainingBuffer !== '' && isset($lines[0])) {
|
||||
$lines[0] = $remainingBuffer.$lines[0];
|
||||
$remainingBuffer = '';
|
||||
}
|
||||
|
||||
if ($lines->last() === '') {
|
||||
$lines = $lines->slice(0, -1);
|
||||
} elseif (! str_ends_with((string) $lines->last(), "\n")) {
|
||||
$remainingBuffer = $lines->pop();
|
||||
}
|
||||
|
||||
$lines
|
||||
->filter(fn (string $line) => $line !== '')
|
||||
->map(fn (string $line) => MessageLogged::fromJson($line))
|
||||
->filter(fn (MessageLogged $messageLogged) => $options->accepts($messageLogged))
|
||||
->each(fn (MessageLogged $messageLogged) => $printer->print($messageLogged));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw command.
|
||||
*/
|
||||
protected function command(File $file): string
|
||||
{
|
||||
return '\\tail -F "'.$file->__toString().'"';
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Env;
|
||||
use Stringable;
|
||||
|
||||
class MessageLogged implements Stringable
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the message logged.
|
||||
*
|
||||
* @param array{__pail: array{origin: array{trace: array<int, array{file: string, line: int}>|null, type: string, queue: string, job: string, command: string, method: string, path: string, auth_id: ?string, auth_email: ?string}}, exception: array{class: string, file: string}} $context
|
||||
*/
|
||||
protected function __construct(
|
||||
protected string $message,
|
||||
protected string $datetime,
|
||||
protected string $levelName,
|
||||
protected array $context,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the message logged from a json string.
|
||||
*/
|
||||
public static function fromJson(string $json): static
|
||||
{
|
||||
/** @var array{message: string, context: array{__pail: array{origin: array{trace: array<int, array{file: string, line: int}>|null, type: string, queue: string, job: string, command: string, method: string, path: string, auth_id: ?string, auth_email: ?string}}, exception: array{class: string, file: string}}, level_name: string, datetime: string} $array */
|
||||
$array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
[
|
||||
'message' => $message,
|
||||
'datetime' => $datetime,
|
||||
'level_name' => $levelName,
|
||||
'context' => $context,
|
||||
] = $array;
|
||||
|
||||
return new static($message, $datetime, $levelName, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's message.
|
||||
*/
|
||||
public function message(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's date.
|
||||
*/
|
||||
public function date(): string
|
||||
{
|
||||
if (Env::get('PAIL_TESTS') ?? false) {
|
||||
return '2024-01-01 03:04:05';
|
||||
}
|
||||
|
||||
$time = Carbon::createFromFormat('Y-m-d\TH:i:s.uP', $this->datetime);
|
||||
|
||||
assert($time instanceof Carbon);
|
||||
|
||||
return $time->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's time.
|
||||
*/
|
||||
public function time(): string
|
||||
{
|
||||
if (Env::get('PAIL_TESTS') ?? false) {
|
||||
return '03:04:05';
|
||||
}
|
||||
|
||||
$time = Carbon::createFromFormat('Y-m-d\TH:i:s.uP', $this->datetime);
|
||||
|
||||
assert($time instanceof Carbon);
|
||||
|
||||
return $time->format('H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's class.
|
||||
*/
|
||||
public function classOrType(): string
|
||||
{
|
||||
return $this->context['exception']['class'] ?? strtoupper($this->levelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's color.
|
||||
*/
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this->levelName) {
|
||||
'DEBUG' => 'gray',
|
||||
'INFO' => 'blue',
|
||||
'NOTICE' => 'yellow',
|
||||
'WARNING' => 'yellow',
|
||||
'ERROR' => 'red',
|
||||
'CRITICAL' => 'red',
|
||||
'ALERT' => 'red',
|
||||
'EMERGENCY' => 'red',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's level.
|
||||
*/
|
||||
public function level(): string
|
||||
{
|
||||
return $this->levelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's file, if any.
|
||||
*/
|
||||
public function file(): ?string
|
||||
{
|
||||
return $this->context['exception']['file'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's auth id.
|
||||
*/
|
||||
public function authId(): ?string
|
||||
{
|
||||
return $this->context['__pail']['origin']['auth_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's origin.
|
||||
*/
|
||||
public function origin(): Origin\Console|Origin\Http|Origin\Queue
|
||||
{
|
||||
return match ($this->context['__pail']['origin']['type']) {
|
||||
'console' => Origin\Console::fromArray($this->context['__pail']['origin']),
|
||||
'queue' => Origin\Queue::fromArray($this->context['__pail']['origin']),
|
||||
default => Origin\Http::fromArray($this->context['__pail']['origin']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's trace, if any.
|
||||
*
|
||||
* @return array<int, array{file: string, line: int}>|null
|
||||
*/
|
||||
public function trace(): ?array
|
||||
{
|
||||
return $this->context['__pail']['origin']['trace'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's context.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function context(): array
|
||||
{
|
||||
return collect($this->context)->except([
|
||||
'__pail',
|
||||
'exception',
|
||||
'userId',
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode([
|
||||
'message' => $this->message,
|
||||
'datetime' => $this->datetime,
|
||||
'level_name' => $this->levelName,
|
||||
'context' => $this->context,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects\Origin;
|
||||
|
||||
class Console
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the console origin.
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $command,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the console origin from the given json string.
|
||||
*
|
||||
* @param array{command?: string} $array
|
||||
*/
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
$command = $array['command'] ?? null;
|
||||
|
||||
return new static($command);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects\Origin;
|
||||
|
||||
class Http
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the http origin.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $method,
|
||||
public string $path,
|
||||
public ?string $authId,
|
||||
public ?string $authEmail,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the http origin from the given json string.
|
||||
*
|
||||
* @param array{method: string, path: string, auth_id: ?string, auth_email: ?string} $array
|
||||
*/
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
['method' => $method, 'path' => $path, 'auth_id' => $authId, 'auth_email' => $authEmail] = $array;
|
||||
|
||||
return new static($method, $path, $authId, $authEmail);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects\Origin;
|
||||
|
||||
class Queue
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the console origin.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $queue,
|
||||
public string $job,
|
||||
public ?string $command,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the queue origin from the given json string.
|
||||
*
|
||||
* @param array{queue: string, job: string, command: ?string} $array
|
||||
*/
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
[
|
||||
'queue' => $queue,
|
||||
'job' => $job,
|
||||
'command' => $command,
|
||||
] = $array;
|
||||
|
||||
return new static($queue, $job, $command);
|
||||
}
|
||||
}
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
Vendored
-97
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"name": "laravel/pao",
|
||||
"description": "Agent-optimized output for PHP testing tools",
|
||||
"keywords": [
|
||||
"php",
|
||||
"ai",
|
||||
"phpunit",
|
||||
"pest",
|
||||
"paratest",
|
||||
"phpstan",
|
||||
"agent",
|
||||
"testing",
|
||||
"dev"
|
||||
],
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pao/issues",
|
||||
"source": "https://github.com/laravel/pao"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/agent-detector": "^2.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.20.0",
|
||||
"laravel/pint": "^1.29.1",
|
||||
"orchestra/testbench": "^10.11.0 || ^11.1.0",
|
||||
"pestphp/pest": "^4.6.3 || ^5.0.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0",
|
||||
"phpstan/phpstan": "^2.1.51",
|
||||
"rector/rector": "^2.4.2",
|
||||
"symfony/process": "^7.4.8 || ^8.1.0",
|
||||
"symfony/var-dumper": "^7.4.8 || ^8.0.8"
|
||||
},
|
||||
"conflict": {
|
||||
"laravel/framework": "<12.0.0",
|
||||
"nunomaduro/collision": "<8.9.3",
|
||||
"phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0",
|
||||
"pestphp/pest": "<4.6.3 || >=6.0.0"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Autoload.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Laravel\\Pao\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Pao\\Laravel\\ServiceProvider"
|
||||
]
|
||||
},
|
||||
"pest": {
|
||||
"plugins": [
|
||||
"Laravel\\Pao\\Drivers\\Pest\\Plugin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"preferred-install": "dist",
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "pint",
|
||||
"refactor": "rector",
|
||||
"test:type-coverage": "pest --type-coverage --min=100",
|
||||
"test:lint": "pint --test",
|
||||
"test:unit": "pest --parallel --coverage --exactly=100",
|
||||
"test:types": "phpstan",
|
||||
"test:refactor": "rector --dry-run",
|
||||
"test": [
|
||||
"@test:refactor",
|
||||
"@test:lint",
|
||||
"@test:type-coverage",
|
||||
"@test:types",
|
||||
"@test:unit"
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @codeCoverageIgnoreStart */
|
||||
|
||||
namespace Laravel\Pao;
|
||||
|
||||
use Laravel\AgentDetector\AgentDetector;
|
||||
|
||||
/** @var array<int, string>|null $argv */
|
||||
$argv = $_SERVER['argv'] ?? null;
|
||||
|
||||
if (! is_array($argv) || $argv === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($_SERVER['PAO_DISABLE'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$agent = AgentDetector::detect();
|
||||
|
||||
if (! $agent->isAgent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_intersect($argv, ['--version', '--help', '-h', 'worker'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($_SERVER['COLLISION_PRINTER']);
|
||||
$_SERVER['PEST_PARALLEL_NO_OUTPUT'] = '1';
|
||||
|
||||
register_shutdown_function(function (): void {
|
||||
if (! Execution::running()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = Execution::current();
|
||||
|
||||
$result = $execution->driver->parse() ?: [];
|
||||
|
||||
$captured = trim(UserFilters\CaptureFilter::output());
|
||||
|
||||
$execution->restoreStdout();
|
||||
|
||||
if ($captured !== '') {
|
||||
$captured = OutputCleaner::clean($captured);
|
||||
|
||||
$lines = array_values(array_filter(
|
||||
array_map(trim(...), explode("\n", $captured)),
|
||||
fn (string $line): bool => $line !== ''
|
||||
&& ! preg_match('/^[.st!]+$/', $line)
|
||||
&& ! preg_match('/^(Tests:|Duration:|Parallel:|Time:|Generating code coverage)\s/', $line)
|
||||
&& ! str_ends_with($line, 'by Sebastian Bergmann and contributors.'),
|
||||
));
|
||||
|
||||
if ($lines !== []) {
|
||||
$result['raw'] = $lines;
|
||||
}
|
||||
}
|
||||
|
||||
if ($result !== []) {
|
||||
$result = ['tool' => $execution->driver->name()] + $result;
|
||||
|
||||
fwrite(STDOUT, json_encode($result, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR).PHP_EOL);
|
||||
}
|
||||
});
|
||||
|
||||
Execution::start($agent, $argv);
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Contracts;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface Driver
|
||||
{
|
||||
public function start(): void;
|
||||
|
||||
public function name(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function parse(): ?array;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Concerns;
|
||||
|
||||
use PHPUnit\Event\Code\TestMethod;
|
||||
use PHPUnit\Event\Telemetry\HRTime;
|
||||
use PHPUnit\Event\Test\Finished;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class ProfileCollector
|
||||
{
|
||||
private static bool $executionStarted = false;
|
||||
|
||||
private static ?HRTime $startTime = null;
|
||||
|
||||
private static float $preparedAt = 0.0;
|
||||
|
||||
/** @var list<array{test: string, file: string, duration_ms: int}> */
|
||||
private static array $entries = [];
|
||||
|
||||
public static function executionStarted(): void
|
||||
{
|
||||
self::$executionStarted = true;
|
||||
}
|
||||
|
||||
public static function hasExecutionStarted(): bool
|
||||
{
|
||||
return self::$executionStarted;
|
||||
}
|
||||
|
||||
public static function startTimer(HRTime $time): void
|
||||
{
|
||||
self::$startTime = $time;
|
||||
}
|
||||
|
||||
public static function startTimerFromNanoseconds(float $nanoseconds): void
|
||||
{
|
||||
$seconds = (int) ($nanoseconds / 1_000_000_000);
|
||||
$nanos = (int) ($nanoseconds - ($seconds * 1_000_000_000));
|
||||
|
||||
self::$startTime = HRTime::fromSecondsAndNanoseconds($seconds, $nanos);
|
||||
}
|
||||
|
||||
public static function durationMs(): int
|
||||
{
|
||||
if (! self::$startTime instanceof HRTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$startNs = (self::$startTime->seconds() * 1_000_000_000) + self::$startTime->nanoseconds();
|
||||
|
||||
return (int) round((hrtime(true) - $startNs) / 1_000_000);
|
||||
}
|
||||
|
||||
public static function prepared(): void
|
||||
{
|
||||
self::$preparedAt = hrtime(true);
|
||||
}
|
||||
|
||||
public static function finished(Finished $event): void
|
||||
{
|
||||
$test = $event->test();
|
||||
|
||||
$file = $test->file();
|
||||
$doubleColonPos = strpos($file, '::');
|
||||
if ($doubleColonPos !== false) {
|
||||
$file = substr($file, 0, $doubleColonPos);
|
||||
}
|
||||
|
||||
self::$entries[] = [
|
||||
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
|
||||
'file' => $file,
|
||||
'duration_ms' => self::$preparedAt > 0
|
||||
? (int) round((hrtime(true) - self::$preparedAt) / 1_000_000)
|
||||
: (int) round($event->telemetryInfo()->durationSincePrevious()->asFloat() * 1000),
|
||||
];
|
||||
|
||||
self::$preparedAt = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{test: string, file: string, duration_ms: int}>
|
||||
*/
|
||||
public static function entries(): array
|
||||
{
|
||||
return self::$entries;
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Concerns;
|
||||
|
||||
use Pest\Plugins\Parallel\Paratest\WrapperRunner;
|
||||
use PHPUnit\Event\Code\TestMethod;
|
||||
use PHPUnit\Event\Code\Throwable;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Event\Test\Errored;
|
||||
use PHPUnit\Event\Test\Finished;
|
||||
use PHPUnit\Event\Test\FinishedSubscriber;
|
||||
use PHPUnit\Event\Test\Prepared;
|
||||
use PHPUnit\Event\Test\PreparedSubscriber;
|
||||
use PHPUnit\Event\TestRunner\ExecutionStarted;
|
||||
use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TestRunner\TestResult\Issues\Issue;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
trait TestResultParsable
|
||||
{
|
||||
public ?TestResult $testResult = null;
|
||||
|
||||
protected function startTimer(): void
|
||||
{
|
||||
try {
|
||||
EventFacade::instance()->registerSubscriber(
|
||||
new class implements ExecutionStartedSubscriber
|
||||
{
|
||||
public function notify(ExecutionStarted $event): void
|
||||
{
|
||||
ProfileCollector::executionStarted();
|
||||
ProfileCollector::startTimer($event->telemetryInfo()->time());
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerProfileSubscriber(): void
|
||||
{
|
||||
/** @var list<string> $argv */
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
|
||||
if (! in_array('--profile', $argv, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventFacade::instance()->registerSubscribers(
|
||||
new class implements PreparedSubscriber
|
||||
{
|
||||
public function notify(Prepared $event): void
|
||||
{
|
||||
ProfileCollector::prepared();
|
||||
}
|
||||
},
|
||||
new class implements FinishedSubscriber
|
||||
{
|
||||
public function notify(Finished $event): void
|
||||
{
|
||||
ProfileCollector::finished($event);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function parse(): ?array
|
||||
{
|
||||
$testResult = $this->resolveTestResult();
|
||||
|
||||
if (! $testResult instanceof TestResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($testResult->numberOfTestsRun() > 0 || ProfileCollector::hasExecutionStarted()) {
|
||||
return $this->parseTestResult($testResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveTestResult(): ?TestResult
|
||||
{
|
||||
if ($this->testResult instanceof TestResult) {
|
||||
return $this->testResult;
|
||||
}
|
||||
|
||||
if (class_exists(WrapperRunner::class, false)
|
||||
&& WrapperRunner::$result instanceof TestResult) {
|
||||
return WrapperRunner::$result;
|
||||
}
|
||||
|
||||
try {
|
||||
return TestResultFacade::result();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function parseTestResult(TestResult $testResult): array
|
||||
{
|
||||
$failedCount = $testResult->numberOfTestFailedEvents();
|
||||
$erroredCount = $testResult->numberOfTestErroredEvents();
|
||||
$skipped = $testResult->numberOfTestSkippedEvents() + $testResult->numberOfTestSkippedByTestSuiteSkippedEvents();
|
||||
$incomplete = $testResult->numberOfTestMarkedIncompleteEvents();
|
||||
$tests = $testResult->numberOfTestsRun();
|
||||
$assertions = $testResult->numberOfAssertions();
|
||||
$deprecations = $testResult->numberOfPhpOrUserDeprecations();
|
||||
$warnings = $testResult->numberOfWarnings();
|
||||
$notices = $testResult->numberOfNotices();
|
||||
$risky = $testResult->numberOfTestsWithTestConsideredRiskyEvents();
|
||||
$ignoredByBaseline = $testResult->numberOfIssuesIgnoredByBaseline();
|
||||
|
||||
$durationMs = ProfileCollector::durationMs();
|
||||
|
||||
/** @var list<array{test: string, file: string, line: int, message: string}> $failureDetails */
|
||||
$failureDetails = [];
|
||||
|
||||
foreach ($testResult->testFailedEvents() as $event) {
|
||||
$test = $event->test();
|
||||
$throwable = $event->throwable();
|
||||
$message = trim($throwable->description());
|
||||
$file = $test->file();
|
||||
$line = $test instanceof TestMethod ? $test->line() : 0;
|
||||
|
||||
[$file, $line] = $this->resolveTestLocation($file, $line, $throwable);
|
||||
|
||||
$failureDetails[] = [
|
||||
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
/** @var list<array{test: string, file: string, line: int, message: string}> $errorDetails */
|
||||
$errorDetails = [];
|
||||
|
||||
foreach ($testResult->testErroredEvents() as $event) {
|
||||
if ($event instanceof Errored) {
|
||||
$test = $event->test();
|
||||
$throwable = $event->throwable();
|
||||
$message = trim($throwable->message());
|
||||
$file = $test->file();
|
||||
$line = $test instanceof TestMethod ? $test->line() : 0;
|
||||
|
||||
[$file, $line] = $this->resolveTestLocation($file, $line, $throwable);
|
||||
|
||||
$errorDetails[] = [
|
||||
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $result */
|
||||
$result = [
|
||||
'result' => $testResult->wasSuccessful() ? 'passed' : 'failed',
|
||||
'tests' => $tests,
|
||||
'passed' => $tests - $failedCount - $erroredCount - $skipped,
|
||||
'assertions' => $assertions,
|
||||
'duration_ms' => $durationMs,
|
||||
];
|
||||
|
||||
if ($failedCount > 0) {
|
||||
$result['failed'] = $failedCount;
|
||||
$result['failures'] = $failureDetails;
|
||||
}
|
||||
|
||||
if ($erroredCount > 0) {
|
||||
$result['errors'] = $erroredCount;
|
||||
$result['error_details'] = $errorDetails;
|
||||
}
|
||||
|
||||
if ($skipped > 0) {
|
||||
$result['skipped'] = $skipped;
|
||||
}
|
||||
|
||||
if ($incomplete > 0) {
|
||||
$result['incomplete'] = $incomplete;
|
||||
}
|
||||
|
||||
if ($deprecations > 0) {
|
||||
$result['deprecations'] = $deprecations;
|
||||
$result['deprecation_details'] = $this->extractIssueDetails(
|
||||
[...$testResult->deprecations(), ...$testResult->phpDeprecations()],
|
||||
);
|
||||
}
|
||||
|
||||
if ($warnings > 0) {
|
||||
$result['warnings'] = $warnings;
|
||||
$result['warning_details'] = $this->extractIssueDetails(
|
||||
[...$testResult->warnings(), ...$testResult->phpWarnings()],
|
||||
);
|
||||
}
|
||||
|
||||
if ($notices > 0) {
|
||||
$result['notices'] = $notices;
|
||||
$result['notice_details'] = $this->extractIssueDetails(
|
||||
[...$testResult->notices(), ...$testResult->phpNotices()],
|
||||
);
|
||||
}
|
||||
|
||||
$phpErrors = $testResult->errors();
|
||||
|
||||
if ($phpErrors !== []) {
|
||||
$result['php_errors'] = count($phpErrors);
|
||||
$result['php_error_details'] = $this->extractIssueDetails($phpErrors);
|
||||
}
|
||||
|
||||
if ($risky > 0) {
|
||||
$result['risky'] = $risky;
|
||||
}
|
||||
|
||||
if ($ignoredByBaseline > 0) {
|
||||
$result['ignored_by_baseline'] = $ignoredByBaseline;
|
||||
}
|
||||
|
||||
$profileEntries = ProfileCollector::entries();
|
||||
|
||||
if ($profileEntries !== []) {
|
||||
usort($profileEntries, fn (array $a, array $b): int => $b['duration_ms'] <=> $a['duration_ms']);
|
||||
$result['profile'] = array_slice($profileEntries, 0, 10);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Issue> $issues
|
||||
* @return list<array{file: string, line: int, message: string}>
|
||||
*/
|
||||
private function extractIssueDetails(array $issues): array
|
||||
{
|
||||
$details = [];
|
||||
|
||||
foreach ($issues as $issue) {
|
||||
$details[] = [
|
||||
'file' => $issue->file(),
|
||||
'line' => $issue->line(),
|
||||
'message' => $issue->description(),
|
||||
];
|
||||
}
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, int}
|
||||
*/
|
||||
private function resolveTestLocation(string $file, int $line, Throwable $throwable): array
|
||||
{
|
||||
$isReal = $line > 0 && ! str_contains($file, "eval()'d code");
|
||||
|
||||
if ($isReal) {
|
||||
return [$file, $line];
|
||||
}
|
||||
|
||||
$text = $throwable->description()."\n".$throwable->stackTrace();
|
||||
|
||||
if (preg_match('/\bat\s+(.+\.php):(\d+)/', $text, $matches) === 1) {
|
||||
return [$matches[1], (int) $matches[2]];
|
||||
}
|
||||
|
||||
if (preg_match('#([\w/\\\\._-]+\.php):(\d+)#', $throwable->stackTrace(), $matches) === 1) {
|
||||
return [$matches[1], (int) $matches[2]];
|
||||
}
|
||||
|
||||
return [$file, $line];
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Paratest;
|
||||
|
||||
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
|
||||
use Laravel\Pao\Drivers\Starter as BaseStarter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class Starter extends BaseStarter
|
||||
{
|
||||
use TestResultParsable;
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'paratest';
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->registerNullFilter();
|
||||
$this->startTimer();
|
||||
$this->silenceStdout();
|
||||
|
||||
/** @var list<string> $serverArgv */
|
||||
$serverArgv = $_SERVER['argv'];
|
||||
|
||||
$argv = $serverArgv;
|
||||
|
||||
$argv[] = '--runner';
|
||||
$argv[] = WrapperRunner::class;
|
||||
|
||||
$_SERVER['argv'] = $argv;
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Paratest;
|
||||
|
||||
use Laravel\Pao\Drivers\Concerns\ProfileCollector;
|
||||
use Laravel\Pao\Execution;
|
||||
use ParaTest\Options;
|
||||
use ParaTest\RunnerInterface;
|
||||
use ParaTest\WrapperRunner\ResultPrinter;
|
||||
use ParaTest\WrapperRunner\SuiteLoader;
|
||||
use ParaTest\WrapperRunner\WrapperRunner as ParatestWrapperRunner;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\Util\ExcludeList;
|
||||
use ReflectionObject;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final readonly class WrapperRunner implements RunnerInterface
|
||||
{
|
||||
private ParatestWrapperRunner $runner;
|
||||
|
||||
public function __construct(
|
||||
Options $options,
|
||||
) {
|
||||
$this->runner = new ParatestWrapperRunner($options, new NullOutput);
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$runner = $this->runner;
|
||||
$r = new ReflectionObject($runner);
|
||||
|
||||
/** @var non-empty-string $directory */
|
||||
$directory = dirname((string) $r->getFileName(), 2);
|
||||
ExcludeList::addDirectory($directory);
|
||||
|
||||
/** @var Options $options */
|
||||
$options = $r->getProperty('options')->getValue($runner);
|
||||
|
||||
/** @var OutputInterface $output */
|
||||
$output = $r->getProperty('output')->getValue($runner);
|
||||
|
||||
/** @var CodeCoverageFilterRegistry $filterRegistry */
|
||||
$filterRegistry = $r->getProperty('codeCoverageFilterRegistry')->getValue($runner);
|
||||
|
||||
$suiteLoader = new SuiteLoader($options, $output, $filterRegistry);
|
||||
|
||||
$result = TestResultFacade::result();
|
||||
|
||||
$r->getProperty('pending')->setValue($runner, $suiteLoader->tests);
|
||||
|
||||
/** @var ResultPrinter $printer */
|
||||
$printer = $r->getProperty('printer')->getValue($runner);
|
||||
$printer->setTestCount($suiteLoader->testCount);
|
||||
$printer->start();
|
||||
|
||||
$startTime = hrtime(true);
|
||||
|
||||
$r->getMethod('startWorkers')->invoke($runner);
|
||||
$r->getMethod('assignAllPendingTests')->invoke($runner);
|
||||
$r->getMethod('waitForAllToFinish')->invoke($runner);
|
||||
|
||||
ProfileCollector::startTimerFromNanoseconds($startTime);
|
||||
|
||||
/** @var list<SplFileInfo> $testResultFiles */
|
||||
$testResultFiles = $r->getProperty('testResultFiles')->getValue($runner);
|
||||
|
||||
$mergedResult = $this->mergeTestResults($result, $testResultFiles);
|
||||
|
||||
if (Execution::running()) {
|
||||
$driver = Execution::current()->driver;
|
||||
|
||||
if ($driver instanceof Starter) {
|
||||
$driver->testResult = $mergedResult;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var int $exitCode */
|
||||
$exitCode = $r->getMethod('complete')->invoke($runner, $result);
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<SplFileInfo> $testResultFiles
|
||||
*/
|
||||
private function mergeTestResults(TestResult $sum, array $testResultFiles): TestResult
|
||||
{
|
||||
foreach ($testResultFiles as $testResultFile) {
|
||||
if (! $testResultFile->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($testResultFile->getPathname());
|
||||
|
||||
if ($contents === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$testResult = unserialize($contents);
|
||||
|
||||
if (! $testResult instanceof TestResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sum = new TestResult(
|
||||
(int) $sum->hasTests() + (int) $testResult->hasTests(),
|
||||
$sum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||
$sum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||
[...$sum->testErroredEvents(), ...$testResult->testErroredEvents()],
|
||||
[...$sum->testFailedEvents(), ...$testResult->testFailedEvents()],
|
||||
array_merge_recursive($sum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), // @phpstan-ignore argument.type
|
||||
[...$sum->testSuiteSkippedEvents(), ...$testResult->testSuiteSkippedEvents()],
|
||||
[...$sum->testSkippedEvents(), ...$testResult->testSkippedEvents()],
|
||||
[...$sum->testMarkedIncompleteEvents(), ...$testResult->testMarkedIncompleteEvents()],
|
||||
array_merge_recursive($sum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), // @phpstan-ignore argument.type
|
||||
array_merge_recursive($sum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), // @phpstan-ignore argument.type
|
||||
array_merge_recursive($sum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), // @phpstan-ignore argument.type
|
||||
array_merge_recursive($sum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), // @phpstan-ignore argument.type
|
||||
[...$sum->testRunnerTriggeredDeprecationEvents(), ...$testResult->testRunnerTriggeredDeprecationEvents()],
|
||||
[...$sum->testRunnerTriggeredNoticeEvents(), ...$testResult->testRunnerTriggeredNoticeEvents()],
|
||||
[...$sum->testRunnerTriggeredWarningEvents(), ...$testResult->testRunnerTriggeredWarningEvents()],
|
||||
[...$sum->errors(), ...$testResult->errors()],
|
||||
[...$sum->deprecations(), ...$testResult->deprecations()],
|
||||
[...$sum->notices(), ...$testResult->notices()],
|
||||
[...$sum->warnings(), ...$testResult->warnings()],
|
||||
[...$sum->phpDeprecations(), ...$testResult->phpDeprecations()],
|
||||
[...$sum->phpNotices(), ...$testResult->phpNotices()],
|
||||
[...$sum->phpWarnings(), ...$testResult->phpWarnings()],
|
||||
$sum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
||||
);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Pest;
|
||||
|
||||
use Laravel\Pao\Execution;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class Plugin implements HandlesArguments
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $arguments
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function handleArguments(array $arguments): array
|
||||
{
|
||||
if (! Execution::running()) {
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
$arguments[] = '--no-output';
|
||||
$arguments[] = '--no-progress';
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Pest;
|
||||
|
||||
use Laravel\Pao\Drivers\Concerns\ProfileCollector;
|
||||
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
|
||||
use Laravel\Pao\Drivers\Starter as BaseStarter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class Starter extends BaseStarter
|
||||
{
|
||||
use TestResultParsable;
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'pest';
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->registerNullFilter();
|
||||
$this->startTimer();
|
||||
$this->saveStdout();
|
||||
$this->silenceStdout();
|
||||
|
||||
/** @var list<string> $argv */
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
|
||||
if (in_array('--parallel', $argv, true)) {
|
||||
ProfileCollector::startTimerFromNanoseconds(hrtime(true));
|
||||
} else {
|
||||
$this->registerProfileSubscriber();
|
||||
}
|
||||
}
|
||||
}
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Phpstan;
|
||||
|
||||
use Laravel\Pao\Drivers\Starter as BaseStarter;
|
||||
use Laravel\Pao\UserFilters\CaptureFilter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class Starter extends BaseStarter
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'phpstan';
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->registerNullFilter();
|
||||
$this->silenceStderr();
|
||||
|
||||
/** @var array<int, string> $argv */
|
||||
$argv = $_SERVER['argv'];
|
||||
$argv = $this->ensureErrorFormatJson($argv);
|
||||
$argv = $this->ensureNoProgress($argv);
|
||||
$_SERVER['argv'] = $argv;
|
||||
|
||||
$this->silenceStdout();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function parse(): ?array
|
||||
{
|
||||
$captured = trim(CaptureFilter::output());
|
||||
|
||||
CaptureFilter::reset();
|
||||
|
||||
if ($captured === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$start = strpos($captured, '{');
|
||||
|
||||
if ($start !== false && $start > 0) {
|
||||
$captured = substr($captured, $start);
|
||||
}
|
||||
|
||||
/** @var array<string, mixed>|null $data */
|
||||
$data = json_decode($captured, associative: true);
|
||||
|
||||
if (! is_array($data) || ! isset($data['totals'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>> $errorDetails */
|
||||
$errorDetails = [];
|
||||
$totalFileErrors = 0;
|
||||
|
||||
/** @var array<string, array{errors: int, messages: list<array{message: string, line: int, identifier?: string, ignorable?: bool, tip?: string}>}> $files */
|
||||
$files = is_array($data['files'] ?? null) ? $data['files'] : [];
|
||||
|
||||
foreach ($files as $file => $fileData) {
|
||||
foreach ($fileData['messages'] as $message) {
|
||||
$totalFileErrors++;
|
||||
|
||||
$detail = [
|
||||
'line' => $message['line'],
|
||||
'message' => $message['message'],
|
||||
'identifier' => $message['identifier'] ?? 'unknown',
|
||||
];
|
||||
|
||||
if (isset($message['ignorable']) && $message['ignorable'] === false) {
|
||||
$detail['ignorable'] = false;
|
||||
}
|
||||
|
||||
if (isset($message['tip']) && $message['tip'] !== '') {
|
||||
$detail['tip'] = $message['tip'];
|
||||
}
|
||||
|
||||
$errorDetails[$file][] = $detail;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var list<string> $errors */
|
||||
$errors = is_array($data['errors'] ?? null) ? $data['errors'] : [];
|
||||
|
||||
/** @var list<string> $generalErrors */
|
||||
$generalErrors = array_values(array_filter($errors, static fn (string $error): bool => $error !== ''));
|
||||
|
||||
$totalErrors = $totalFileErrors + count($generalErrors);
|
||||
|
||||
/** @var array<string, mixed> $result */
|
||||
$result = [
|
||||
'result' => $totalErrors > 0 ? 'failed' : 'passed',
|
||||
'errors' => $totalErrors,
|
||||
];
|
||||
|
||||
if ($errorDetails !== []) {
|
||||
$verbose = $this->isVerbose();
|
||||
$limit = 30;
|
||||
|
||||
if (! $verbose && $totalFileErrors > $limit) {
|
||||
$result['error_details'] = $this->truncateGrouped($errorDetails, $limit);
|
||||
$result['truncated'] = true;
|
||||
$result['hint'] = 'Pass -v to see all errors.';
|
||||
} else {
|
||||
$result['error_details'] = $errorDetails;
|
||||
}
|
||||
}
|
||||
|
||||
if ($generalErrors !== []) {
|
||||
$result['general_errors'] = $generalErrors;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>> $grouped
|
||||
* @return array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>>
|
||||
*/
|
||||
private function truncateGrouped(array $grouped, int $limit): array
|
||||
{
|
||||
$result = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($grouped as $file => $errors) {
|
||||
foreach ($errors as $error) {
|
||||
if ($count >= $limit) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
$result[$file][] = $error;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function isVerbose(): bool
|
||||
{
|
||||
/** @var array<int, string> $argv */
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
|
||||
foreach ($argv as $arg) {
|
||||
if (in_array($arg, ['-v', '-vv', '-vvv', '--verbose'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $argv
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function ensureErrorFormatJson(array $argv): array
|
||||
{
|
||||
$filtered = [];
|
||||
$skipNext = false;
|
||||
|
||||
foreach ($argv as $arg) {
|
||||
if ($skipNext) {
|
||||
$skipNext = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($arg, '--error-format=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--error-format') {
|
||||
$skipNext = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered[] = $arg;
|
||||
}
|
||||
|
||||
$filtered[] = '--error-format=json';
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $argv
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function ensureNoProgress(array $argv): array
|
||||
{
|
||||
if (! in_array('--no-progress', $argv, true)) {
|
||||
$argv[] = '--no-progress';
|
||||
}
|
||||
|
||||
return $argv;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers\Phpunit;
|
||||
|
||||
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
|
||||
use Laravel\Pao\Drivers\Starter as BaseStarter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class Starter extends BaseStarter
|
||||
{
|
||||
use TestResultParsable;
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'phpunit';
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->registerNullFilter();
|
||||
$this->startTimer();
|
||||
$this->registerProfileSubscriber();
|
||||
|
||||
/** @var list<string> $serverArgv */
|
||||
$serverArgv = $_SERVER['argv'];
|
||||
|
||||
$argv = $serverArgv;
|
||||
|
||||
if (! in_array('--no-output', $argv, true)) {
|
||||
$argv[] = '--no-output';
|
||||
}
|
||||
|
||||
$_SERVER['argv'] = $argv;
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Drivers;
|
||||
|
||||
use Laravel\Pao\Contracts\Driver;
|
||||
use Laravel\Pao\Execution;
|
||||
use Laravel\Pao\UserFilters\CaptureFilter;
|
||||
use Laravel\Pao\UserFilters\NullFilter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
abstract class Starter implements Driver
|
||||
{
|
||||
protected function registerNullFilter(): void
|
||||
{
|
||||
if (! in_array('agent_output_null', stream_get_filters(), true)) {
|
||||
stream_filter_register('agent_output_null', NullFilter::class);
|
||||
}
|
||||
}
|
||||
|
||||
protected function silenceStdout(): void
|
||||
{
|
||||
if (! in_array('agent_output_capture', stream_get_filters(), true)) {
|
||||
stream_filter_register('agent_output_capture', CaptureFilter::class);
|
||||
}
|
||||
|
||||
CaptureFilter::reset();
|
||||
|
||||
$execution = Execution::current();
|
||||
|
||||
$execution->filter = stream_filter_append(STDOUT, 'agent_output_capture', STREAM_FILTER_WRITE) ?: null;
|
||||
}
|
||||
|
||||
protected function silenceStderr(): void
|
||||
{
|
||||
stream_filter_append(STDERR, 'agent_output_null', STREAM_FILTER_WRITE);
|
||||
}
|
||||
|
||||
protected function saveStdout(): void
|
||||
{
|
||||
$execution = Execution::current();
|
||||
|
||||
$execution->stdout = fopen('php://stdout', 'w') ?: STDOUT;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class ShouldNotHappenException extends RuntimeException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('This should not have happened. Please report this issue at [https://github.com/laravel/pao/issues/new].');
|
||||
}
|
||||
}
|
||||
Vendored
-100
@@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao;
|
||||
|
||||
use Laravel\AgentDetector\AgentResult;
|
||||
use Laravel\Pao\Contracts\Driver;
|
||||
use Laravel\Pao\Exceptions\ShouldNotHappenException;
|
||||
use Laravel\Pao\UserFilters\CaptureFilter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @phpstan-type TestDetail array{test: string, file: string, line: int, message: string}
|
||||
* @phpstan-type ProfileEntry array{test: string, file: string, duration_ms: int}
|
||||
* @phpstan-type Result array{result: 'passed'|'failed', tests: int, passed: int, duration_ms: int, failed?: int, failures?: list<TestDetail>, errors?: int, error_details?: list<TestDetail>, skipped?: int, profile?: list<ProfileEntry>, raw?: list<string>}
|
||||
*/
|
||||
final class Execution
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
|
||||
/**
|
||||
* @param resource|null $stdout
|
||||
* @param resource|null $filter
|
||||
*/
|
||||
private function __construct(
|
||||
public readonly AgentResult $agent,
|
||||
public readonly Driver $driver,
|
||||
public mixed $stdout = null,
|
||||
public mixed $filter = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $argv
|
||||
*/
|
||||
public static function start(AgentResult $agent, array $argv): void
|
||||
{
|
||||
if (self::running()) {
|
||||
throw new ShouldNotHappenException;
|
||||
}
|
||||
|
||||
$binary = basename($argv[0] ?? '');
|
||||
|
||||
$starter = match ($binary) {
|
||||
'paratest' => new Drivers\Paratest\Starter,
|
||||
'pest' => new Drivers\Pest\Starter,
|
||||
'phpstan', 'phpstan.phar' => new Drivers\Phpstan\Starter,
|
||||
'phpunit' => new Drivers\Phpunit\Starter,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($starter instanceof Driver) {
|
||||
self::$instance = new self(
|
||||
$agent,
|
||||
$starter,
|
||||
);
|
||||
|
||||
$starter->start();
|
||||
}
|
||||
}
|
||||
|
||||
public static function running(): bool
|
||||
{
|
||||
return self::$instance instanceof Execution;
|
||||
}
|
||||
|
||||
public static function current(): self
|
||||
{
|
||||
return self::$instance ?? throw new ShouldNotHappenException;
|
||||
}
|
||||
|
||||
public function restoreStdout(): void
|
||||
{
|
||||
if (is_resource($this->filter)) {
|
||||
stream_filter_remove($this->filter);
|
||||
|
||||
$this->filter = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function flushStdout(): void
|
||||
{
|
||||
if (! is_resource($this->filter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$captured = CaptureFilter::output();
|
||||
|
||||
$this->restoreStdout();
|
||||
|
||||
if ($captured !== '') {
|
||||
fwrite(STDOUT, $captured);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Laravel;
|
||||
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Laravel\Pao\OutputCleaner;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class PaoOutputStyle extends OutputStyle
|
||||
{
|
||||
private static ?OutputFormatter $formatter = null;
|
||||
|
||||
public function __construct(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$output->setDecorated(false);
|
||||
|
||||
parent::__construct($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|iterable<string> $messages
|
||||
*/
|
||||
#[\Override]
|
||||
public function write(string|iterable $messages, bool $newline = false, int $options = 0): void
|
||||
{
|
||||
parent::write($this->clean($messages), $newline, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|iterable<string> $messages
|
||||
*/
|
||||
#[\Override]
|
||||
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void
|
||||
{
|
||||
parent::writeln($this->clean($messages), $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|iterable<string> $messages
|
||||
* @return string|list<string>
|
||||
*/
|
||||
private function clean(string|iterable $messages): string|array
|
||||
{
|
||||
$formatter = self::$formatter ??= new OutputFormatter(false);
|
||||
$strip = fn (string $m): string => OutputCleaner::clean((string) $formatter->format($m));
|
||||
|
||||
if (is_string($messages)) {
|
||||
return $strip($messages);
|
||||
}
|
||||
|
||||
return array_values(array_map($strip, [...$messages]));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\Laravel;
|
||||
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
|
||||
use Laravel\AgentDetector\AgentDetector;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class ServiceProvider extends LaravelServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if (isset($_SERVER['PAO_DISABLE'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->app->runningUnitTests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! AgentDetector::detect()->isAgent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->app->bind(OutputStyle::class, PaoOutputStyle::class);
|
||||
|
||||
/** @var Dispatcher $events */
|
||||
$events = $this->app->make(Dispatcher::class);
|
||||
$events->listen(CommandStarting::class, function (CommandStarting $event): void {
|
||||
$event->output->setDecorated(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class OutputCleaner
|
||||
{
|
||||
public static function clean(string $output): string
|
||||
{
|
||||
$output = (string) preg_replace('/\e\[[0-9;]*[A-Za-z]/', '', $output);
|
||||
$output = (string) preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $output);
|
||||
$output = (string) preg_replace('/\x{FFFD}/u', '', $output);
|
||||
$output = (string) preg_replace('/[─━│┌┐└┘├┤┬┴┼▓░▒═║╔╗╚╝╠╣╦╩╬➜▶►⚠✖✔●◆■▪→←↑↓▕⨯✕]+/u', '', $output);
|
||||
$output = (string) preg_replace('/\.{3,}/', '..', $output);
|
||||
$output = (string) preg_replace('/[ \t]+/', ' ', $output);
|
||||
|
||||
return (string) preg_replace('/\n\s*\n/', "\n", $output);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\UserFilters;
|
||||
|
||||
use php_user_filter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class CaptureFilter extends php_user_filter
|
||||
{
|
||||
private static string $captured = '';
|
||||
|
||||
/**
|
||||
* @param resource $in
|
||||
* @param resource $out
|
||||
* @param int $consumed
|
||||
*/
|
||||
public function filter($in, $out, &$consumed, bool $closing): int // @pest-ignore-type
|
||||
{
|
||||
while ($bucket = stream_bucket_make_writeable($in)) {
|
||||
/** @var int $datalen */
|
||||
$datalen = $bucket->datalen;
|
||||
$consumed += $datalen;
|
||||
|
||||
/** @var string $data */
|
||||
$data = $bucket->data;
|
||||
self::$captured .= $data;
|
||||
|
||||
$bucket->data = '';
|
||||
stream_bucket_append($out, $bucket);
|
||||
}
|
||||
|
||||
return PSFS_PASS_ON;
|
||||
}
|
||||
|
||||
public static function output(): string
|
||||
{
|
||||
return self::$captured;
|
||||
}
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$captured = '';
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Pao\UserFilters;
|
||||
|
||||
use php_user_filter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class NullFilter extends php_user_filter
|
||||
{
|
||||
public function filter($in, $out, &$consumed, bool $closing): int // @pest-ignore-type
|
||||
{
|
||||
while ($bucket = stream_bucket_make_writeable($in)) {
|
||||
/** @var int $datalen */
|
||||
$datalen = $bucket->datalen;
|
||||
$consumed += $datalen;
|
||||
$bucket->data = '';
|
||||
stream_bucket_append($out, $bucket);
|
||||
}
|
||||
|
||||
return PSFS_PASS_ON;
|
||||
}
|
||||
}
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
-62
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"description": "An opinionated code formatter for PHP.",
|
||||
"keywords": ["php", "format", "formatter", "lint", "linter", "dev"],
|
||||
"homepage": "https://laravel.com",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2.0",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xml": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.95.1",
|
||||
"illuminate/view": "^12.56.0",
|
||||
"larastan/larastan": "^3.9.6",
|
||||
"laravel-zero/framework": "^12.1.0",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"pestphp/pest": "^3.8.6",
|
||||
"shipfastlabs/agent-detector": "^1.1.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Scripts\\": "scripts/",
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"optimize-autoloader": true,
|
||||
"platform": {
|
||||
"php": "8.2.0"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"bin": ["builds/pint"]
|
||||
}
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use App\Fixers\TypeAnnotationsOnlyFixer;
|
||||
use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
|
||||
use PhpCsFixer\RuleSet\RuleSetInterface;
|
||||
use Symfony\Component\Finder\Finder as SymfonyFinder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
/**
|
||||
* This overrides the default "FixerFactory" to register
|
||||
* Pint's custom fixers alongside the built-in ones,
|
||||
* ensuring they are available in parallel worker processes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class FixerFactory
|
||||
{
|
||||
private FixerNameValidator $nameValidator;
|
||||
|
||||
/**
|
||||
* @var list<FixerInterface>
|
||||
*/
|
||||
private array $fixers = [];
|
||||
|
||||
/**
|
||||
* @var array<string, FixerInterface>
|
||||
*/
|
||||
private array $fixersByName = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->nameValidator = new FixerNameValidator;
|
||||
}
|
||||
|
||||
public function setWhitespacesConfig(WhitespacesFixerConfig $config): self
|
||||
{
|
||||
foreach ($this->fixers as $fixer) {
|
||||
if ($fixer instanceof WhitespacesAwareFixerInterface) {
|
||||
$fixer->setWhitespacesConfig($config);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<FixerInterface>
|
||||
*/
|
||||
public function getFixers(): array
|
||||
{
|
||||
$this->fixers = Utils::sortFixers($this->fixers);
|
||||
|
||||
return $this->fixers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerBuiltInFixers(): self
|
||||
{
|
||||
static $builtInFixers = null;
|
||||
|
||||
if ($builtInFixers === null) {
|
||||
/** @var list<class-string<FixerInterface>> */
|
||||
$builtInFixers = [];
|
||||
|
||||
$finder = SymfonyFinder::create()->files()
|
||||
->in(dirname(__DIR__).'/vendor/friendsofphp/php-cs-fixer/src/Fixer')
|
||||
->exclude(['Internal'])
|
||||
->name('*Fixer.php')
|
||||
->depth(1);
|
||||
|
||||
/** @var SplFileInfo $file */
|
||||
foreach ($finder as $file) {
|
||||
$relativeNamespace = $file->getRelativePath();
|
||||
$fixerClass = 'PhpCsFixer\Fixer\\'.($relativeNamespace !== '' ? $relativeNamespace.'\\' : '').$file->getBasename('.php');
|
||||
$builtInFixers[] = $fixerClass;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($builtInFixers as $class) {
|
||||
/** @var FixerInterface */
|
||||
$fixer = new $class;
|
||||
$this->registerFixer($fixer, false);
|
||||
}
|
||||
|
||||
$this->registerCustomFixers([
|
||||
new TypeAnnotationsOnlyFixer,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<FixerInterface> $fixers
|
||||
* @return $this
|
||||
*/
|
||||
public function registerCustomFixers(iterable $fixers): self
|
||||
{
|
||||
foreach ($fixers as $fixer) {
|
||||
$this->registerFixer($fixer, true);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerFixer(FixerInterface $fixer, bool $isCustom): self
|
||||
{
|
||||
$name = $fixer->getName();
|
||||
|
||||
if (isset($this->fixersByName[$name])) {
|
||||
throw new \UnexpectedValueException(\sprintf('Fixer named "%s" is already registered.', $name));
|
||||
}
|
||||
|
||||
if (! $this->nameValidator->isValid($name, $isCustom)) {
|
||||
throw new \UnexpectedValueException(\sprintf('Fixer named "%s" has invalid name.', $name));
|
||||
}
|
||||
|
||||
$this->fixers[] = $fixer;
|
||||
$this->fixersByName[$name] = $fixer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function useRuleSet(RuleSetInterface $ruleSet): self
|
||||
{
|
||||
$fixers = [];
|
||||
$fixersByName = [];
|
||||
$fixerConflicts = [];
|
||||
|
||||
$fixerNames = array_keys($ruleSet->getRules());
|
||||
foreach ($fixerNames as $name) {
|
||||
if (! \array_key_exists($name, $this->fixersByName)) {
|
||||
throw new \UnexpectedValueException(\sprintf('Rule "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
$fixer = $this->fixersByName[$name];
|
||||
$config = $ruleSet->getRuleConfiguration($name);
|
||||
|
||||
if ($config !== null) {
|
||||
if ($fixer instanceof ConfigurableFixerInterface) {
|
||||
if (\count($config) < 1) {
|
||||
throw new InvalidFixerConfigurationException($fixer->getName(), 'Configuration must be an array and may not be empty.');
|
||||
}
|
||||
|
||||
$fixer->configure($config);
|
||||
} else {
|
||||
throw new InvalidFixerConfigurationException($fixer->getName(), 'Is not configurable.');
|
||||
}
|
||||
}
|
||||
|
||||
$fixers[] = $fixer;
|
||||
$fixersByName[$name] = $fixer;
|
||||
$conflicts = array_values(array_intersect($this->getFixersConflicts($fixer), $fixerNames));
|
||||
|
||||
if (\count($conflicts) > 0) {
|
||||
$fixerConflicts[$name] = $conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($fixerConflicts) > 0) {
|
||||
throw new \UnexpectedValueException($this->generateConflictMessage($fixerConflicts));
|
||||
}
|
||||
|
||||
$this->fixers = $fixers;
|
||||
$this->fixersByName = $fixersByName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasRule(string $name): bool
|
||||
{
|
||||
return isset($this->fixersByName[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function getFixersConflicts(FixerInterface $fixer): array
|
||||
{
|
||||
return [
|
||||
'blank_lines_before_namespace' => [
|
||||
'no_blank_lines_before_namespace',
|
||||
'single_blank_line_before_namespace',
|
||||
],
|
||||
'no_blank_lines_before_namespace' => ['single_blank_line_before_namespace'],
|
||||
'single_import_per_statement' => ['group_import'],
|
||||
][$fixer->getName()] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<string>> $fixerConflicts
|
||||
*/
|
||||
private function generateConflictMessage(array $fixerConflicts): string
|
||||
{
|
||||
$message = 'Rule contains conflicting fixers:';
|
||||
$report = [];
|
||||
|
||||
foreach ($fixerConflicts as $fixer => $fixers) {
|
||||
$report[$fixer] = array_values(array_filter(
|
||||
$fixers,
|
||||
static fn (string $candidate): bool => ! \array_key_exists($candidate, $report) || ! \in_array($fixer, $report[$candidate], true),
|
||||
));
|
||||
|
||||
if (\count($report[$fixer]) > 0) {
|
||||
$message .= \sprintf("\n- \"%s\" with %s", $fixer, Utils::naturalLanguageJoin($report[$fixer]));
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Runner\Parallel;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2012+ Fabien Potencier, Dariusz Rumiński
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
use Illuminate\Support\ProcessUtils;
|
||||
use PhpCsFixer\Runner\RunnerConfig;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
/**
|
||||
* This overrides the default "ProcessFactory" to allow for
|
||||
* customization of the command-line arguments that better
|
||||
* suit the needs of the laravel pint package.
|
||||
*
|
||||
* @author Greg Korba <greg@codito.dev>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ProcessFactory
|
||||
{
|
||||
public function create(
|
||||
LoopInterface $loop,
|
||||
InputInterface $input,
|
||||
RunnerConfig $runnerConfig,
|
||||
ProcessIdentifier $identifier,
|
||||
int $serverPort
|
||||
): Process {
|
||||
$commandArgs = $this->getCommandArgs($serverPort, $identifier, $input, $runnerConfig);
|
||||
|
||||
return new Process(
|
||||
implode(' ', $commandArgs),
|
||||
$loop,
|
||||
$runnerConfig->getParallelConfig()->getProcessTimeout()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getCommandArgs(int $serverPort, ProcessIdentifier $identifier, InputInterface $input, RunnerConfig $runnerConfig): array
|
||||
{
|
||||
$phpBinary = (new PhpExecutableFinder)->find(false);
|
||||
|
||||
if ($phpBinary === false) {
|
||||
throw new ParallelisationException('Cannot find PHP executable.');
|
||||
}
|
||||
|
||||
$mainScript = $_SERVER['argv'][0];
|
||||
|
||||
$commandArgs = [
|
||||
ProcessUtils::escapeArgument($phpBinary),
|
||||
ProcessUtils::escapeArgument($mainScript),
|
||||
'worker',
|
||||
'--port',
|
||||
(string) $serverPort,
|
||||
'--identifier',
|
||||
ProcessUtils::escapeArgument($identifier->toString()),
|
||||
];
|
||||
|
||||
if ($runnerConfig->isDryRun()) {
|
||||
$commandArgs[] = '--dry-run';
|
||||
}
|
||||
|
||||
if (filter_var($input->getOption('diff'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
$commandArgs[] = '--diff';
|
||||
}
|
||||
|
||||
if (filter_var($input->getOption('stop-on-violation'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
$commandArgs[] = '--stop-on-violation';
|
||||
}
|
||||
|
||||
foreach (['allow-risky', 'config', 'rules', 'using-cache', 'cache-file'] as $option) {
|
||||
$optionValue = $input->getOption($option);
|
||||
|
||||
if ($optionValue !== null) {
|
||||
$commandArgs[] = "--{$option}";
|
||||
$commandArgs[] = ProcessUtils::escapeArgument($optionValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $commandArgs;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user