diff --git a/src/Exceptions/DriverNotFoundException.php b/src/Exceptions/DriverNotFoundException.php index 1ea354a..83d2995 100644 --- a/src/Exceptions/DriverNotFoundException.php +++ b/src/Exceptions/DriverNotFoundException.php @@ -10,10 +10,28 @@ class DriverNotFoundException extends AetherException { /** - * Create an exception for an unknown driver name. + * Create an exception for a driver name a caller asked for explicitly. + * + * @param list $available The names that do resolve, so a typo is easy to spot. */ - public static function forDriver(string $name): self + public static function forDriver(string $name, array $available = []): self { - return new self("Quantum driver [{$name}] is not registered. Check your 'aether.default' configuration."); + $known = $available === [] + ? '' + : ' Registered drivers: '.implode(', ', array_map(static fn (string $driver): string => "'{$driver}'", $available)).'.'; + + return new self( + "Quantum driver [{$name}] is not registered.{$known} Register a custom one with Quantum::extend('{$name}', ...) or check the name for a typo." + ); + } + + /** + * Create an exception for a default driver that resolves to nothing. + */ + public static function forDefaultDriver(string $name): self + { + return new self( + "Quantum driver [{$name}] is configured as the default but is not registered. Check the 'aether.default' setting (AETHER_DRIVER) in config/aether.php, or register the driver with Quantum::extend()." + ); } } diff --git a/src/QuantumManager.php b/src/QuantumManager.php index b288eaa..af74752 100644 --- a/src/QuantumManager.php +++ b/src/QuantumManager.php @@ -32,7 +32,11 @@ class QuantumManager extends Manager */ public function getDefaultDriver(): string { - return $this->config->get('aether.default', 'local'); + // A null or blank aether.default (an empty AETHER_DRIVER= line) must + // still resolve to a driver, not surface later as a TypeError. + $default = $this->config->get('aether.default'); + + return is_string($default) && $default !== '' ? $default : 'local'; } /** @@ -139,17 +143,39 @@ public function bridge(): PythonBridge */ protected function createDriver($driver) { - if (isset($this->customCreators[$driver])) { - return $this->callCustomCreator($driver); + // Manager has already turned an enum into its value, which may be an int. + $name = (string) $driver; + + if (isset($this->customCreators[$name])) { + return $this->callCustomCreator($name); } - $method = 'create'.Str::studly($driver).'Driver'; + $method = 'create'.Str::studly($name).'Driver'; - if (method_exists($this, $method)) { + if ($name !== '' && method_exists($this, $method)) { return $this->$method(); } - throw DriverNotFoundException::forDriver($driver); + // Manager resolves a null argument to the default before calling us, so + // an unknown name that equals the default points at configuration; any + // other unknown name was asked for explicitly by the caller. + throw $name === $this->getDefaultDriver() + ? DriverNotFoundException::forDefaultDriver($name) + : DriverNotFoundException::forDriver($name, $this->availableDrivers()); + } + + /** + * The driver names that resolve today: the built-ins plus every extend()ed one. + * + * @return list + */ + private function availableDrivers(): array + { + return array_values(array_unique([ + 'local', + 'aws', + ...array_map(strval(...), array_keys($this->customCreators)), + ])); } /** diff --git a/tests/Feature/QuantumManagerTest.php b/tests/Feature/QuantumManagerTest.php index 0f99548..2b7a2a6 100644 --- a/tests/Feature/QuantumManagerTest.php +++ b/tests/Feature/QuantumManagerTest.php @@ -24,6 +24,54 @@ expect(app(QuantumManager::class)->driver('aws'))->toBeInstanceOf(AwsBraketDriver::class); }); +it('blames the driver name, not the default setting, when an explicit driver is unknown', function () { + config(['aether.default' => 'local']); + + try { + app(QuantumManager::class)->driver('ionq'); + $this->fail('Expected DriverNotFoundException.'); + } catch (DriverNotFoundException $e) { + expect($e->getMessage()) + ->toContain("Quantum::extend('ionq'") + ->not->toContain('aether.default'); + } +}); + +it('names the extended drivers in the message for an unknown explicit driver', function () { + $manager = app(QuantumManager::class); + $manager->extend('ionq', fn () => Mockery::mock(QuantumDevice::class)); + + try { + $manager->driver('ionk'); + $this->fail('Expected DriverNotFoundException.'); + } catch (DriverNotFoundException $e) { + expect($e->getMessage())->toContain("'ionq'"); + } +}); + +it('falls back to the local driver when aether.default is null or blank', function (mixed $default) { + config(['aether.default' => $default]); + + expect(app(QuantumManager::class)->driver())->toBeInstanceOf(LocalSimulatorDriver::class); +})->with(['null' => [null], 'blank' => ['']]); + +it('still throws DriverNotFoundException for an unknown driver when aether.default is null', function () { + config(['aether.default' => null]); + + expect(fn () => app(QuantumManager::class)->driver('ionq'))->toThrow(DriverNotFoundException::class); +}); + +it('blames the aether.default setting when the configured default driver is unknown', function () { + config(['aether.default' => 'ionq']); + + try { + app(QuantumManager::class)->driver(); + $this->fail('Expected DriverNotFoundException.'); + } catch (DriverNotFoundException $e) { + expect($e->getMessage())->toContain('aether.default'); + } +}); + it('throws DriverNotFoundException for unknown driver', function () { app(QuantumManager::class)->driver('unknown'); })->throws(DriverNotFoundException::class); diff --git a/tests/Unit/Exceptions/ExceptionHierarchyTest.php b/tests/Unit/Exceptions/ExceptionHierarchyTest.php index 10064e7..74e9176 100644 --- a/tests/Unit/Exceptions/ExceptionHierarchyTest.php +++ b/tests/Unit/Exceptions/ExceptionHierarchyTest.php @@ -77,11 +77,30 @@ expect(is_subclass_of(DriverNotFoundException::class, AetherException::class))->toBeTrue(); }); -it('for driver includes driver name', function (): void { +it('for driver includes driver name and points at registration, not at the default setting', function (): void { $exception = DriverNotFoundException::forDriver('braket'); expect($exception)->toBeInstanceOf(DriverNotFoundException::class); - expect($exception->getMessage())->toContain('braket'); + expect($exception->getMessage()) + ->toContain('braket') + ->toContain("Quantum::extend('braket'") + ->not->toContain('aether.default'); +}); + +it('for driver lists the registered drivers when given', function (): void { + $exception = DriverNotFoundException::forDriver('ionk', ['local', 'aws', 'ionq']); + + expect($exception->getMessage())->toContain("Registered drivers: 'local', 'aws', 'ionq'."); +}); + +it('for default driver points at the aether.default setting', function (): void { + $exception = DriverNotFoundException::forDefaultDriver('braket'); + + expect($exception)->toBeInstanceOf(DriverNotFoundException::class); + expect($exception->getMessage()) + ->toContain('braket') + ->toContain('aether.default') + ->toContain('AETHER_DRIVER'); }); // -------------------------------------------------------------------------