Skip to content
Open
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
24 changes: 21 additions & 3 deletions src/Exceptions/DriverNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> $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()."
);
}
}
38 changes: 32 additions & 6 deletions src/QuantumManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

/**
Expand Down Expand Up @@ -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<string>
*/
private function availableDrivers(): array
{
return array_values(array_unique([
'local',
'aws',
...array_map(strval(...), array_keys($this->customCreators)),
]));
}

/**
Expand Down
48 changes: 48 additions & 0 deletions tests/Feature/QuantumManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 21 additions & 2 deletions tests/Unit/Exceptions/ExceptionHierarchyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

// -------------------------------------------------------------------------
Expand Down