• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orchestral / testbench-core / 13198058225

07 Feb 2025 10:34AM UTC coverage: 92.08% (+0.4%) from 91.687%
13198058225

Pull #292

github

web-flow
Merge cab9eba4a into 432920503
Pull Request #292: Supports Parallel Testing on PHPUnit 12.0

1523 of 1654 relevant lines covered (92.08%)

74.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.75
/src/functions.php
1
<?php
2

3
namespace Orchestra\Testbench;
4

5
use Closure;
6
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
7
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
8
use Illuminate\Filesystem\Filesystem;
9
use Illuminate\Foundation\Application;
10
use Illuminate\Routing\Router;
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\Collection;
13
use Illuminate\Support\ProcessUtils;
14
use Illuminate\Support\Str;
15
use Illuminate\Testing\PendingCommand;
16
use InvalidArgumentException;
17
use Orchestra\Testbench\Foundation\Config;
18
use Orchestra\Testbench\Foundation\Env;
19
use PHPUnit\Runner\Version;
20
use RuntimeException;
21
use Symfony\Component\Process\Process;
22

23
/**
24
 * Create Laravel application instance.
25
 *
26
 * @api
27
 *
28
 * @param  string|null  $basePath
29
 * @param  (callable(\Illuminate\Foundation\Application):(void))|null  $resolvingCallback
30
 * @param  array{extra?: array{providers?: array, dont-discover?: array, env?: array}, load_environment_variables?: bool, enabled_package_discoveries?: bool}  $options
31
 * @param  \Orchestra\Testbench\Foundation\Config|null  $config
32
 * @return \Orchestra\Testbench\Foundation\Application
33
 */
34
function container(
35
    ?string $basePath = null,
36
    ?callable $resolvingCallback = null,
37
    array $options = [],
38
    ?Config $config = null
39
): Foundation\Application {
40
    if ($config instanceof Config) {
3✔
41
        return Foundation\Application::makeFromConfig($config, $resolvingCallback, $options);
×
42
    }
43

44
    return Foundation\Application::make($basePath, $resolvingCallback, $options);
3✔
45
}
46

47
/**
48
 * Run artisan command.
49
 *
50
 * @api
51
 *
52
 * @param  \Orchestra\Testbench\Contracts\TestCase|\Illuminate\Contracts\Foundation\Application  $context
53
 * @param  string  $command
54
 * @param  array<string, mixed>  $parameters
55
 * @return int
56
 */
57
function artisan(Contracts\TestCase|ApplicationContract $context, string $command, array $parameters = []): int
58
{
59
    if ($context instanceof ApplicationContract) {
15✔
60
        return $context->make(ConsoleKernel::class)->call($command, $parameters);
1✔
61
    }
62

63
    $command = $context->artisan($command, $parameters);
15✔
64

65
    return $command instanceof PendingCommand ? $command->run() : $command;
15✔
66
}
67

68
/**
69
 * Run remote action using Testbench CLI.
70
 *
71
 * @api
72
 *
73
 * @param  array<int, string>|string  $command
74
 * @param  array<string, mixed>|string  $env
75
 * @param  bool|null  $tty
76
 * @return \Symfony\Component\Process\Process
77
 */
78
function remote(array|string $command, array|string $env = [], ?bool $tty = null): Process
79
{
80
    $binary = \defined('TESTBENCH_DUSK') ? 'testbench-dusk' : 'testbench';
11✔
81

82
    $commander = is_file($vendorBin = package_path('vendor', 'bin', $binary))
11✔
83
        ? ProcessUtils::escapeArgument((string) $vendorBin)
×
84
        : $binary;
11✔
85

86
    if (\is_string($env)) {
11✔
87
        $env = ['APP_ENV' => $env];
×
88
    }
89

90
    Arr::add($env, 'TESTBENCH_PACKAGE_REMOTE', '(true)');
11✔
91

92
    $process = Process::fromShellCommandline(
11✔
93
        command: Arr::join([php_binary(true), $commander, ...Arr::wrap($command)], ' '),
11✔
94
        cwd: package_path(),
11✔
95
        env: array_merge(defined_environment_variables(), $env)
11✔
96
    );
11✔
97

98
    if (\is_bool($tty)) {
11✔
99
        $process->setTty($tty);
×
100
    }
101

102
    return $process;
11✔
103
}
104

105
/**
106
 * Run callback only once.
107
 *
108
 * @api
109
 *
110
 * @param  mixed  $callback
111
 * @return \Closure():mixed
112
 */
113
function once($callback): Closure
114
{
115
    $response = new Support\UndefinedValue;
192✔
116

117
    return function () use ($callback, &$response) {
192✔
118
        if ($response instanceof Support\UndefinedValue) {
192✔
119
            $response = value($callback) ?? null;
192✔
120
        }
121

122
        return $response;
192✔
123
    };
192✔
124
}
125

126
/**
127
 * Register after resolving callback.
128
 *
129
 * @api
130
 *
131
 * @param  \Illuminate\Contracts\Foundation\Application  $app
132
 * @param  string  $name
133
 * @param  (\Closure(object, \Illuminate\Contracts\Foundation\Application):(mixed))|null  $callback
134
 * @return void
135
 */
136
function after_resolving(ApplicationContract $app, string $name, ?Closure $callback = null): void
137
{
138
    $app->afterResolving($name, $callback);
192✔
139

140
    if ($app->resolved($name)) {
192✔
141
        value($callback, $app->make($name), $app);
1✔
142
    }
143
}
144

145
/**
146
 * Load migration paths.
147
 *
148
 * @api
149
 *
150
 * @param  \Illuminate\Contracts\Foundation\Application  $app
151
 * @param  array<int, string>|string  $paths
152
 * @return void
153
 */
154
function load_migration_paths(ApplicationContract $app, array|string $paths): void
155
{
156
    after_resolving($app, 'migrator', static function ($migrator) use ($paths) {
50✔
157
        foreach (Arr::wrap($paths) as $path) {
15✔
158
            /** @var \Illuminate\Database\Migrations\Migrator $migrator */
159
            $migrator->path($path);
15✔
160
        }
161
    });
50✔
162
}
163

164
/**
165
 * Get defined environment variables.
166
 *
167
 * @api
168
 *
169
 * @return array<string, mixed>
170
 */
171
function defined_environment_variables(): array
172
{
173
    return Collection::make(array_merge($_SERVER, $_ENV))
11✔
174
        ->keys()
11✔
175
        ->mapWithKeys(static fn (string $key) => [$key => Env::forward($key)])
11✔
176
        ->unless(
11✔
177
            Env::has('TESTBENCH_WORKING_PATH'), static fn ($env) => $env->put('TESTBENCH_WORKING_PATH', package_path())
11✔
178
        )->all();
11✔
179
}
180

181
/**
182
 * Get default environment variables.
183
 *
184
 * @api
185
 *
186
 * @param  iterable<string, mixed>  $variables
187
 * @return array<int, string>
188
 */
189
function parse_environment_variables($variables): array
190
{
191
    return Collection::make($variables)
4✔
192
        ->transform(static function ($value, $key) {
4✔
193
            if (\is_bool($value) || \in_array($value, ['true', 'false'])) {
4✔
194
                $value = \in_array($value, [true, 'true']) ? '(true)' : '(false)';
4✔
195
            } elseif (\is_null($value) || \in_array($value, ['null'])) {
1✔
196
                $value = '(null)';
1✔
197
            } else {
198
                $value = $key === 'APP_DEBUG' ? \sprintf('(%s)', Str::of($value)->ltrim('(')->rtrim(')')) : "'{$value}'";
1✔
199
            }
200

201
            return "{$key}={$value}";
4✔
202
        })->values()->all();
4✔
203
}
204

205
/**
206
 * Refresh router lookups.
207
 *
208
 * @api
209
 *
210
 * @param  \Illuminate\Routing\Router  $router
211
 * @return void
212
 */
213
function refresh_router_lookups(Router $router): void
214
{
215
    $router->getRoutes()->refreshNameLookups();
192✔
216
}
217

218
/**
219
 * Transform realpath to alias path.
220
 *
221
 * @api
222
 *
223
 * @param  string  $path
224
 * @param  string|null  $workingPath
225
 * @return string
226
 */
227
function transform_realpath_to_relative(string $path, ?string $workingPath = null, string $prefix = ''): string
228
{
229
    $separator = DIRECTORY_SEPARATOR;
13✔
230

231
    if (! \is_null($workingPath)) {
13✔
232
        return str_replace(rtrim($workingPath, $separator).$separator, $prefix.$separator, $path);
1✔
233
    }
234

235
    $laravelPath = base_path();
12✔
236
    $workbenchPath = workbench_path();
12✔
237
    $packagePath = package_path();
12✔
238

239
    return match (true) {
240
        str_starts_with($path, $laravelPath) => str_replace($laravelPath.$separator, '@laravel'.$separator, $path),
12✔
241
        str_starts_with($path, $workbenchPath) => str_replace($workbenchPath.$separator, '@workbench'.$separator, $path),
8✔
242
        str_starts_with($path, $packagePath) => str_replace($packagePath.$separator, '.'.$separator, $path),
8✔
243
        ! empty($prefix) => implode($separator, [$prefix, ltrim($path, $separator)]),
8✔
244
        default => $path,
12✔
245
    };
246
}
247

248
/**
249
 * Transform relative path.
250
 *
251
 * @api
252
 *
253
 * @param  string  $path
254
 * @param  string  $workingPath
255
 * @return string
256
 */
257
function transform_relative_path(string $path, string $workingPath): string
258
{
259
    return str_starts_with($path, './')
53✔
260
        ? rtrim($workingPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.mb_substr($path, 2)
8✔
261
        : $path;
53✔
262
}
263

264
/**
265
 * Get the default skeleton path.
266
 *
267
 * @api
268
 *
269
 * @no-named-arguments
270
 *
271
 * @param  array<int, string|null>|string  ...$path
272
 * @return string
273
 */
274
function default_skeleton_path(array|string $path = ''): string
275
{
276
    return (string) realpath(join_paths(__DIR__, '..', 'laravel', ...Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path)));
189✔
277
}
278

279
/**
280
 * Get the migration path by type.
281
 *
282
 * @api
283
 *
284
 * @param  string|null  $type
285
 * @return string
286
 *
287
 * @throws \InvalidArgumentException
288
 */
289
function default_migration_path(?string $type = null): string
290
{
291
    $path = realpath(
57✔
292
        \is_null($type) ? base_path('migrations') : base_path(join_paths('migrations', $type))
57✔
293
    );
57✔
294

295
    if ($path === false) {
57✔
296
        throw new InvalidArgumentException(\sprintf('Unable to resolve migration path for type [%s]', $type ?? 'laravel'));
×
297
    }
298

299
    return $path;
57✔
300
}
301

302
/**
303
 * Get the path to the package folder.
304
 *
305
 * @api
306
 *
307
 * @no-named-arguments
308
 *
309
 * @param  array<int, string|null>|string  ...$path
310
 * @return string
311
 */
312
function package_path(array|string $path = ''): string
313
{
314
    $argumentCount = \func_num_args();
87✔
315

316
    $workingPath = \defined('TESTBENCH_WORKING_PATH')
87✔
317
        ? TESTBENCH_WORKING_PATH
49✔
318
        : Env::get('TESTBENCH_WORKING_PATH', getcwd());
38✔
319

320
    if ($argumentCount === 1 && \is_string($path) && str_starts_with($path, './')) {
87✔
321
        return transform_relative_path($path, $workingPath);
7✔
322
    }
323

324
    $path = join_paths(...Arr::wrap($argumentCount > 1 ? \func_get_args() : $path));
87✔
325

326
    return str_starts_with($path, './')
87✔
327
        ? transform_relative_path($path, $workingPath)
×
328
        : join_paths(rtrim($workingPath, DIRECTORY_SEPARATOR), $path);
87✔
329
}
330

331
/**
332
 * Get the workbench configuration.
333
 *
334
 * @api
335
 *
336
 * @return array<string, mixed>
337
 */
338
function workbench(): array
339
{
340
    /** @var \Orchestra\Testbench\Contracts\Config $config */
341
    $config = app()->bound(Contracts\Config::class)
47✔
342
        ? app()->make(Contracts\Config::class)
45✔
343
        : new Foundation\Config;
2✔
344

345
    return $config->getWorkbenchAttributes();
47✔
346
}
347

348
/**
349
 * Get the path to the workbench folder.
350
 *
351
 * @api
352
 *
353
 * @no-named-arguments
354
 *
355
 * @param  array<int, string|null>|string  ...$path
356
 * @return string
357
 */
358
function workbench_path(array|string $path = ''): string
359
{
360
    return package_path('workbench', ...Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path));
65✔
361
}
362

363
/**
364
 * Get the migration path by type.
365
 *
366
 * @api
367
 *
368
 * @param  string|null  $type
369
 * @return string
370
 *
371
 * @throws \InvalidArgumentException
372
 *
373
 * @deprecated
374
 */
375
#[\Deprecated(message: 'Use `Orchestra\Testbench\default_migration_path()` instead', since: '9.5.1')]
376
function laravel_migration_path(?string $type = null): string
377
{
378
    return default_migration_path($type);
1✔
379
}
380

381
/**
382
 * Determine if vendor symlink exists on the laravel application.
383
 *
384
 * @api
385
 *
386
 * @param  \Illuminate\Contracts\Foundation\Application  $app
387
 * @param  string|null  $workingPath
388
 * @return bool
389
 */
390
function laravel_vendor_exists(ApplicationContract $app, ?string $workingPath = null): bool
391
{
392
    $filesystem = new Filesystem;
3✔
393

394
    $appVendorPath = $app->basePath('vendor');
3✔
395
    $workingPath ??= package_path('vendor');
3✔
396

397
    return $filesystem->isFile(join_paths($appVendorPath, 'autoload.php')) &&
3✔
398
        $filesystem->hash(join_paths($appVendorPath, 'autoload.php')) === $filesystem->hash(join_paths($workingPath, 'autoload.php'));
3✔
399
}
400

401
/**
402
 * Laravel version compare.
403
 *
404
 * @api
405
 *
406
 * @template TOperator of string|null
407
 *
408
 * @param  string  $version
409
 * @param  string|null  $operator
410
 * @return int|bool
411
 *
412
 * @phpstan-param  TOperator  $operator
413
 *
414
 * @phpstan-return (TOperator is null ? int : bool)
415
 */
416
function laravel_version_compare(string $version, ?string $operator = null): int|bool
417
{
418
    /** @var string $laravel */
419
    $laravel = transform(
5✔
420
        Application::VERSION,
5✔
421
        fn (string $version) => $version === '11.x-dev' ? '11.0.0' : $version, // @phpstan-ignore identical.alwaysFalse
5✔
422
    );
5✔
423

424
    if (\is_null($operator)) {
5✔
425
        return version_compare($laravel, $version);
1✔
426
    }
427

428
    return version_compare($laravel, $version, $operator);
5✔
429
}
430

431
/**
432
 * PHPUnit version compare.
433
 *
434
 * @api
435
 *
436
 * @template TOperator of string|null
437
 *
438
 * @param  string  $version
439
 * @param  string|null  $operator
440
 * @return int|bool
441
 *
442
 * @throws \RuntimeException
443
 *
444
 * @phpstan-param  TOperator  $operator
445
 *
446
 * @phpstan-return (TOperator is null ? int : bool)
447
 */
448
function phpunit_version_compare(string $version, ?string $operator = null): int|bool
449
{
450
    if (! class_exists(Version::class)) {
2✔
451
        throw new RuntimeException('Unable to verify PHPUnit version');
×
452
    }
453

454
    /** @var string $phpunit */
455
    $phpunit = transform(
2✔
456
        Version::id(),
2✔
457
        fn (string $version) => match (true) {
2✔
458
            str_starts_with($version, '12.0-') => '12.0.0',
2✔
459
            str_starts_with($version, '11.5-') => '11.5.0',
2✔
460
            default => $version,
2✔
461
        }
462
    );
2✔
463

464
    if (\is_null($operator)) {
2✔
465
        return version_compare($phpunit, $version);
1✔
466
    }
467

468
    return version_compare($phpunit, $version, $operator);
2✔
469
}
470

471
/**
472
 * Determine the PHP Binary.
473
 *
474
 * @api
475
 *
476
 * @param  bool  $escape
477
 * @return string
478
 */
479
function php_binary(bool $escape = false): string
480
{
481
    $phpBinary = (new Support\PhpExecutableFinder)->find(false) ?: 'php';
11✔
482

483
    return $escape === true ? ProcessUtils::escapeArgument((string) $phpBinary) : $phpBinary;
11✔
484
}
485

486
/**
487
 * Join the given paths together.
488
 *
489
 * @param  string|null  $basePath
490
 * @param  string  ...$paths
491
 * @return string
492
 */
493
function join_paths(?string $basePath, string ...$paths): string
494
{
495
    foreach ($paths as $index => $path) {
195✔
496
        if (empty($path) && $path !== '0') {
195✔
497
            unset($paths[$index]);
189✔
498
        } else {
499
            $paths[$index] = DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR);
195✔
500
        }
501
    }
502

503
    return $basePath.implode('', $paths);
195✔
504
}
505

506
/**
507
 * Ensure the provided `$app` return an instance of Laravel application or throw an exception.
508
 *
509
 * @internal
510
 *
511
 * @param  \Illuminate\Foundation\Application|null  $app
512
 * @param  string|null  $caller
513
 * @return \Illuminate\Foundation\Application
514
 *
515
 * @throws \Orchestra\Testbench\Exceptions\ApplicationNotAvailableException
516
 */
517
function laravel_or_fail($app, ?string $caller = null): Application
518
{
519
    if ($app instanceof Application) {
186✔
520
        return $app;
186✔
521
    }
522

523
    if (\is_null($caller)) {
1✔
524
        $caller = transform(debug_backtrace()[1] ?? null, function ($debug) {
1✔
525
            /** @phpstan-ignore isset.offset */
526
            if (isset($debug['class']) && isset($debug['function'])) {
1✔
527
                return \sprintf('%s::%s', $debug['class'], $debug['function']);
1✔
528
            }
529

530
            /** @phpstan-ignore offsetAccess.notFound */
531
            return $debug['function'];
×
532
        });
1✔
533
    }
534

535
    throw Exceptions\ApplicationNotAvailableException::make($caller);
1✔
536
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc