Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -851,27 +851,29 @@ Some test files reference classes defined in other test files. Laravel gets away

#### Helper Class Namespacing

Laravel tests define helper classes (models, stubs) with generic names like `User`, `Post`, `Comment`. When multiple test files use the same namespace and define classes with the same name, PHP throws "Cannot redeclare class" errors.
Laravel tests often define helper classes (models, stubs) with generic names like `User`, `Post`, or `Comment`. When multiple test files use the same namespace and define classes with the same name, PHP throws "Cannot redeclare class" errors.

**Use test-specific namespaces** (matching Laravel's pattern):
**Use test-specific namespaces only for collision-prone helper classes** (matching Laravel's pattern):

```php
// WRONG - shared namespace causes conflicts
// WRONG - shared namespace causes conflicts for generic helper names
namespace Hypervel\Tests\Integration\Database;

class EloquentDeleteTest extends DatabaseTestCase { ... }
class Comment extends Model {} // Conflicts with Comment in other files!

// CORRECT - test-specific namespace isolates classes
// CORRECT - test-specific namespace isolates generic helper names
namespace Hypervel\Tests\Integration\Database\EloquentDeleteTest;

class EloquentDeleteTest extends DatabaseTestCase { ... }
class Comment extends Model {} // No conflict - different namespace
```

The namespace includes the test class name as the final segment. This means:
- Each test file has its own namespace
- Helper classes can use simple names (`Comment`, `Post`, `User`)
Use this when helper classes have generic names likely to appear in other test files. Do not add extra namespaces for helper classes whose names already include the tested feature or package context, such as `FailingHorizonInstallCommand` or `MissingProviderTelescopeInstallCommand`.

When a test-specific namespace is needed, the namespace includes the test class name as the final segment. This means:
- Each affected test file has its own namespace
- Generic helper classes can use simple names (`Comment`, `Post`, `User`)
- No `$table` properties needed (Eloquent derives `comments` from `Comment`)
- No explicit foreign keys needed (Eloquent derives `user_id` from `User`)

Expand All @@ -897,7 +899,7 @@ This list is exhaustive. Any other missing functionality requires investigation
6. Add type declarations to model properties
7. Fix mock types (PDO, QueryBuilder, Grammar, etc.)
8. Add `->andReturnSelf()` to chained method mocks
9. Use test-specific namespace if file defines helper classes — avoids "Cannot redeclare class" errors when multiple test files define classes with the same name (e.g., `...Database\EloquentDeleteTest`)
9. Use a test-specific namespace only when helper classes have generic, collision-prone names — already-specific helper names do not need extra namespace ceremony.
10. Remove tests for unsupported features (SQL Server/MongoDB/DynamoDB databases, Memcached/DynamoDB/MongoDB cache, dynamic connections)
11. Run tests and fix any remaining type errors

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
},
"require": {
"php": ">=8.4",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-hash": "*",
Expand Down
5 changes: 1 addition & 4 deletions src/boost/docs/testbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -1015,9 +1015,6 @@ The `package:test` command runs your package tests through PHPUnit or ParaTest:
vendor/bin/testbench package:test
```

> [!NOTE]
> The `package:test` command requires `nunomaduro/collision` as a development dependency. If it is not installed, Testbench will offer to install it for you.

To run tests in parallel, pass the `--parallel` option:

```shell
Expand All @@ -1029,7 +1026,6 @@ The command supports the following options:
| Option | Description |
| --- | --- |
| `--without-tty` | Disable TTY output. |
| `--compact` | Use the compact Collision printer output. |
| `--configuration=` | Read PHPUnit configuration from the given XML file. |
| `--coverage` | Collect code coverage. |
| `--min=` | Fail when coverage is below the given percentage. |
Expand All @@ -1038,6 +1034,7 @@ The command supports the following options:
| `--recreate-databases` | Re-create test databases before parallel testing. |
| `--drop-databases` | Drop test databases after parallel testing. |
| `--without-databases` | Disable parallel database setup. |
| `--without-cache` | Disable parallel cache prefix setup. |

Additional PHPUnit and ParaTest arguments may be passed after the command options:

Expand Down
4 changes: 1 addition & 3 deletions src/boost/docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,9 @@ php artisan test --testsuite=Feature --stop-on-failure
<a name="running-tests-in-parallel"></a>
### Running Tests in Parallel

By default, Hypervel and PHPUnit execute your tests sequentially within a single process. However, you may greatly reduce the amount of time it takes to run your tests by running tests simultaneously across multiple processes. To get started, you should install the `brianium/paratest` Composer package as a "dev" dependency. Then, include the `--parallel` option when executing the `test` Artisan command:
By default, Hypervel and PHPUnit execute your tests sequentially within a single process. However, you may greatly reduce the amount of time it takes to run your tests by running tests simultaneously across multiple processes. Hypervel's application skeleton includes the `brianium/paratest` Composer package as a development dependency, so you may include the `--parallel` option when executing the `test` Artisan command:

```shell
composer require brianium/paratest --dev

php artisan test --parallel
```

Expand Down
5 changes: 0 additions & 5 deletions src/boost/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,8 @@

## Support

## Telescope

- Port Laravel's `telescope:install` command. The copied Telescope docs document `php artisan telescope:install`, but Hypervel currently only registers `telescope:publish`, `telescope:clear`, `telescope:pause`, `telescope:prune`, and `telescope:resume`. Hypervel already publishes the provider stub, config, and migrations under the `telescope-provider`, `telescope-config`, and `telescope-migrations` tags. Correct fix: port Laravel Telescope's install command with Hypervel namespaces, publish those three tags, register `App\Providers\TelescopeServiceProvider` in `bootstrap/providers.php` via `Hypervel\Support\ServiceProvider::addProviderToBootstrapFile()`, register the command in `Hypervel\Telescope\TelescopeServiceProvider`, and add command coverage.

## Testing

- Port an app-facing `php artisan test` command. The copied testing docs document `php artisan test`, including `--parallel`, `--coverage`, `--min`, `--profile`, `--recreate-databases`, `--drop-databases`, `--without-databases`, `--without-cache`, and ParaTest pass-through options such as `--processes`, but Hypervel currently ships only `make:test` for applications and `package:test` for Testbench package development. The underlying machinery already exists: `Hypervel\Testing\ParallelRunner`, `Hypervel\Testing\ParallelTesting`, parallel database / cache / view handling, and Collision's coverage / printer support used by Testbench's `package:test` command. Correct fix: add a Hypervel application test command, or a Hypervel Collision adapter, that shells out to PHPUnit / ParaTest using `Hypervel\Testing\ParallelRunner`, sets the `HYPERVEL_PARALLEL_TESTING_*` environment variables, preserves PHPUnit / ParaTest pass-through arguments, and port the matching command coverage.
- Port the `#[UnitTest]` testing attribute and no-boot test lifecycle. The copied testing docs reference `Hypervel\Foundation\Testing\Attributes\UnitTest` to skip booting the application for a single test method, but Hypervel currently has no `UnitTest` attribute and `Hypervel\Foundation\Testing\TestCase::setUp()` / `tearDown()` always call the framework lifecycle. Laravel implements this with `Illuminate\Foundation\Testing\Attributes\UnitTest` and a memoized `withoutBootingFramework()` check on the current test method. Correct fix: add the attribute class, add the per-method reflection check to Hypervel's test case lifecycle, skip application boot / teardown when present, preserve `RunTestsInCoroutine` behavior unless deliberately disabled by the test class, and port Laravel's coverage.
- Update the PHPUnit `make:test --unit` stub to use Hypervel's coroutine-aware application test case. The testing docs recommend extending `Tests\TestCase` for Hypervel application tests so they run through `RunTestsInCoroutine`, but `src/foundation/src/Console/stubs/test.unit.stub` currently extends raw `PHPUnit\Framework\TestCase`. Correct fix: change the unit stub to extend `Tests\TestCase`, keep `#[UnitTest]` as an optional per-method optimization for tests that should skip booting the application, and update the generator coverage.

Expand Down
5 changes: 4 additions & 1 deletion src/console/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"hypervel/reflection": "^0.4",
"hypervel/support": "^0.4"
},
"suggest": {
"nunomaduro/collision": "Required to render enhanced console exception output."
},
"config": {
"sort-packages": true
},
Expand All @@ -61,4 +64,4 @@
"dev-main": "0.4-dev"
}
}
}
}
22 changes: 22 additions & 0 deletions src/console/src/CollisionInspector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Hypervel\Console;

use Throwable;
use Whoops\Exception\Inspector as BaseInspector;

final class CollisionInspector extends BaseInspector
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
/**
* Get the backtrace from an exception.
*
* @param Throwable $exception
* @return array<int, array<string, mixed>>
*/
protected function getTrace($exception)
{
return $exception->getTrace();
}
}
3 changes: 1 addition & 2 deletions src/console/src/ErrorRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Hypervel\Console;

use NunoMaduro\Collision\Adapters\Laravel\Inspector;
use NunoMaduro\Collision\Provider;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Input\ArgvInput;
Expand Down Expand Up @@ -60,7 +59,7 @@ protected function renderCollision(Throwable $throwable): void
$handler = (new Provider)
->getHandler()
->setOutput($this->output);
$handler->setInspector(new Inspector($throwable));
$handler->setInspector(new CollisionInspector($throwable));
$handler->handle();
}
}
79 changes: 56 additions & 23 deletions src/horizon/src/Console/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,47 +25,80 @@ class InstallCommand extends Command
/**
* Execute the console command.
*/
public function handle(): void
public function handle(): int
{
$this->components->info('Installing Horizon resources.');

collect([
'Service Provider' => fn () => $this->callSilent('vendor:publish', ['--tag' => 'horizon-provider']) == 0,
'Configuration' => fn () => $this->callSilent('vendor:publish', ['--tag' => 'horizon-config']) == 0,
])->each(fn ($task, $description) => $this->components->task($description, $task));
if (! $this->publishResource('Service Provider', 'horizon-provider')
|| ! $this->publishResource('Configuration', 'horizon-config')) {
return self::FAILURE;
}

$this->registerHorizonServiceProvider();
if (! $this->registerHorizonServiceProvider()) {
return self::FAILURE;
}

$this->components->info('Horizon scaffolding installed successfully.');

return self::SUCCESS;
}

/**
* Register the Horizon service provider in the application configuration file.
* Publish a Horizon resource.
*/
protected function registerHorizonServiceProvider(): void
protected function publishResource(string $description, string $tag): bool
{
$published = false;

$this->components->task($description, function () use (&$published, $tag): bool {
return $published = $this->callSilent('vendor:publish', ['--tag' => $tag]) === 0;
});

if (! $published) {
$this->components->error("Unable to publish Horizon {$description}.");
}

return $published;
}

/**
* Register the Horizon service provider in the application bootstrap file.
*/
protected function registerHorizonServiceProvider(): bool
{
$namespace = Str::replaceLast('\\', '', $this->hypervel->getNamespace());
$providerPath = $this->hypervel->path('Providers/HorizonServiceProvider.php');

if (! is_file($providerPath) || ! is_readable($providerPath)) {
$this->components->error('HorizonServiceProvider file was not published.');

if (file_exists($this->hypervel->bootstrapPath('providers.php'))) {
ServiceProvider::addProviderToBootstrapFile("{$namespace}\\Providers\\HorizonServiceProvider");
} else {
$appConfig = file_get_contents(config_path('app.php'));
return false;
}

if (Str::contains($appConfig, $namespace . '\Providers\HorizonServiceProvider::class')) {
return;
}
$contents = file_get_contents($providerPath);

file_put_contents(config_path('app.php'), str_replace(
"{$namespace}\\Providers\\EventServiceProvider::class," . PHP_EOL,
"{$namespace}\\Providers\\EventServiceProvider::class," . PHP_EOL . " {$namespace}\\Providers\\HorizonServiceProvider::class," . PHP_EOL,
$appConfig
));
if ($contents === false) {
$this->components->error('Unable to read the HorizonServiceProvider file.');

return false;
}

file_put_contents(app_path('Providers/HorizonServiceProvider.php'), str_replace(
if (file_put_contents($providerPath, str_replace(
'namespace App\Providers;',
"namespace {$namespace}\\Providers;",
file_get_contents(app_path('Providers/HorizonServiceProvider.php'))
));
$contents,
)) === false) {
$this->components->error('Unable to update the HorizonServiceProvider namespace.');

return false;
}

if (! ServiceProvider::addProviderToBootstrapFile("{$namespace}\\Providers\\HorizonServiceProvider")) {
$this->components->error('Unable to register HorizonServiceProvider in bootstrap/providers.php.');

return false;
}

return true;
}
}
Loading