|
|
<?php
/** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */namespace Test\TaskProcessing;
use OC\AppFramework\Bootstrap\Coordinator;use OC\AppFramework\Bootstrap\RegistrationContext;use OC\AppFramework\Bootstrap\ServiceRegistration;use OC\EventDispatcher\EventDispatcher;use OC\TaskProcessing\Db\TaskMapper;use OC\TaskProcessing\Manager;use OC\TaskProcessing\RemoveOldTasksBackgroundJob;use OC\TaskProcessing\SynchronousBackgroundJob;use OCP\App\IAppManager;use OCP\AppFramework\Utility\ITimeFactory;use OCP\BackgroundJob\IJobList;use OCP\EventDispatcher\IEventDispatcher;use OCP\Files\AppData\IAppDataFactory;use OCP\Files\Config\ICachedMountInfo;use OCP\Files\Config\IUserMountCache;use OCP\Files\File;use OCP\Files\IRootFolder;use OCP\Http\Client\IClientService;use OCP\IAppConfig;use OCP\ICacheFactory;use OCP\IConfig;use OCP\IDBConnection;use OCP\IServerContainer;use OCP\IUser;use OCP\IUserManager;use OCP\IUserSession;use OCP\L10N\IFactory;use OCP\Server;use OCP\TaskProcessing\EShapeType;use OCP\TaskProcessing\Events\GetTaskProcessingProvidersEvent;use OCP\TaskProcessing\Events\TaskFailedEvent;use OCP\TaskProcessing\Events\TaskSuccessfulEvent;use OCP\TaskProcessing\Exception\NotFoundException;use OCP\TaskProcessing\Exception\PreConditionNotMetException;use OCP\TaskProcessing\Exception\ProcessingException;use OCP\TaskProcessing\Exception\UnauthorizedException;use OCP\TaskProcessing\Exception\ValidationException;use OCP\TaskProcessing\IManager;use OCP\TaskProcessing\IProvider;use OCP\TaskProcessing\ISynchronousProvider;use OCP\TaskProcessing\ITaskType;use OCP\TaskProcessing\ITriggerableProvider;use OCP\TaskProcessing\ShapeDescriptor;use OCP\TaskProcessing\Task;use OCP\TaskProcessing\TaskTypes\TextToImage;use OCP\TaskProcessing\TaskTypes\TextToText;use OCP\TaskProcessing\TaskTypes\TextToTextSummary;use OCP\TextProcessing\SummaryTaskType;use PHPUnit\Framework\Constraint\IsInstanceOf;use Psr\Log\LoggerInterface;use Test\BackgroundJob\DummyJobList;
class AudioToImage implements ITaskType { public const ID = 'test:audiotoimage';
public function getId(): string { return self::ID; }
public function getName(): string { return self::class; }
public function getDescription(): string { return self::class; }
public function getInputShape(): array { return [ 'audio' => new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio), ]; }
public function getOutputShape(): array { return [ 'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image), ]; }}
class AsyncProvider implements IProvider { public function getId(): string { return 'test:sync:success'; }
public function getName(): string { return self::class; }
public function getTaskTypeId(): string { return AudioToImage::ID; }
public function getExpectedRuntime(): int { return 10; }
public function getOptionalInputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function getOptionalOutputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function getInputShapeEnumValues(): array { return []; }
public function getInputShapeDefaults(): array { return []; }
public function getOptionalInputShapeEnumValues(): array { return []; }
public function getOptionalInputShapeDefaults(): array { return []; }
public function getOutputShapeEnumValues(): array { return []; }
public function getOptionalOutputShapeEnumValues(): array { return []; }}
class SuccessfulSyncProvider implements IProvider, ISynchronousProvider { public const ID = 'test:sync:success';
public function getId(): string { return self::ID; }
public function getName(): string { return self::class; }
public function getTaskTypeId(): string { return TextToText::ID; }
public function getExpectedRuntime(): int { return 10; }
public function getOptionalInputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function getOptionalOutputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function process(?string $userId, array $input, callable $reportProgress): array { return ['output' => $input['input']]; }
public function getInputShapeEnumValues(): array { return []; }
public function getInputShapeDefaults(): array { return []; }
public function getOptionalInputShapeEnumValues(): array { return []; }
public function getOptionalInputShapeDefaults(): array { return []; }
public function getOutputShapeEnumValues(): array { return []; }
public function getOptionalOutputShapeEnumValues(): array { return []; }}
class FailingSyncProvider implements IProvider, ISynchronousProvider { public const ERROR_MESSAGE = 'Failure'; public function getId(): string { return 'test:sync:fail'; }
public function getName(): string { return self::class; }
public function getTaskTypeId(): string { return TextToText::ID; }
public function getExpectedRuntime(): int { return 10; }
public function getOptionalInputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function getOptionalOutputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function process(?string $userId, array $input, callable $reportProgress): array { throw new ProcessingException(self::ERROR_MESSAGE); }
public function getInputShapeEnumValues(): array { return []; }
public function getInputShapeDefaults(): array { return []; }
public function getOptionalInputShapeEnumValues(): array { return []; }
public function getOptionalInputShapeDefaults(): array { return []; }
public function getOutputShapeEnumValues(): array { return []; }
public function getOptionalOutputShapeEnumValues(): array { return []; }}
class BrokenSyncProvider implements IProvider, ISynchronousProvider { public function getId(): string { return 'test:sync:broken-output'; }
public function getName(): string { return self::class; }
public function getTaskTypeId(): string { return TextToText::ID; }
public function getExpectedRuntime(): int { return 10; }
public function getOptionalInputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function getOptionalOutputShape(): array { return [ 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; }
public function process(?string $userId, array $input, callable $reportProgress): array { return []; }
public function getInputShapeEnumValues(): array { return []; }
public function getInputShapeDefaults(): array { return []; }
public function getOptionalInputShapeEnumValues(): array { return []; }
public function getOptionalInputShapeDefaults(): array { return []; }
public function getOutputShapeEnumValues(): array { return []; }
public function getOptionalOutputShapeEnumValues(): array { return []; }}
class SuccessfulTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider { public bool $ran = false;
public function getName(): string { return 'TEST Vanilla LLM Provider'; }
public function process(string $prompt): string { $this->ran = true; return $prompt . ' Summarize'; }
public function getTaskType(): string { return SummaryTaskType::class; }}
class FailingTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider { public bool $ran = false;
public function getName(): string { return 'TEST Vanilla LLM Provider'; }
public function process(string $prompt): string { $this->ran = true; throw new \Exception('ERROR'); }
public function getTaskType(): string { return SummaryTaskType::class; }}
class SuccessfulTextToImageProvider implements \OCP\TextToImage\IProvider { public bool $ran = false;
public function getId(): string { return 'test:successful'; }
public function getName(): string { return 'TEST Provider'; }
public function generate(string $prompt, array $resources): void { $this->ran = true; foreach ($resources as $resource) { fwrite($resource, 'test'); } }
public function getExpectedRuntime(): int { return 1; }}
class FailingTextToImageProvider implements \OCP\TextToImage\IProvider { public bool $ran = false;
public function getId(): string { return 'test:failing'; }
public function getName(): string { return 'TEST Provider'; }
public function generate(string $prompt, array $resources): void { $this->ran = true; throw new \RuntimeException('ERROR'); }
public function getExpectedRuntime(): int { return 1; }}
class ExternalProvider implements IProvider { public const ID = 'event:external:provider'; public const TASK_TYPE_ID = 'event:external:tasktype';
public function getId(): string { return self::ID; } public function getName(): string { return 'External Provider via Event'; } public function getTaskTypeId(): string { return self::TASK_TYPE_ID; } public function getExpectedRuntime(): int { return 5; } public function getOptionalInputShape(): array { return []; } public function getOptionalOutputShape(): array { return []; } public function getInputShapeEnumValues(): array { return []; } public function getInputShapeDefaults(): array { return []; } public function getOptionalInputShapeEnumValues(): array { return []; } public function getOptionalInputShapeDefaults(): array { return []; } public function getOutputShapeEnumValues(): array { return []; } public function getOptionalOutputShapeEnumValues(): array { return []; }}
class ExternalTriggerableProvider implements ITriggerableProvider { public const ID = 'event:external:provider:triggerable'; public const TASK_TYPE_ID = TextToText::ID;
public function getId(): string { return self::ID; } public function getName(): string { return 'External Triggerable Provider via Event'; }
public function getTaskTypeId(): string { return self::TASK_TYPE_ID; }
public function trigger(): void { } public function getExpectedRuntime(): int { return 5; } public function getOptionalInputShape(): array { return []; } public function getOptionalOutputShape(): array { return []; } public function getInputShapeEnumValues(): array { return []; } public function getInputShapeDefaults(): array { return []; } public function getOptionalInputShapeEnumValues(): array { return []; } public function getOptionalInputShapeDefaults(): array { return []; } public function getOutputShapeEnumValues(): array { return []; } public function getOptionalOutputShapeEnumValues(): array { return []; }}
class ConflictingExternalProvider implements IProvider { // Same ID as SuccessfulSyncProvider
public const ID = 'test:sync:success'; public const TASK_TYPE_ID = 'event:external:tasktype'; // Can be different task type
public function getId(): string { return self::ID; } public function getName(): string { return 'Conflicting External Provider'; } public function getTaskTypeId(): string { return self::TASK_TYPE_ID; } public function getExpectedRuntime(): int { return 50; } public function getOptionalInputShape(): array { return []; } public function getOptionalOutputShape(): array { return []; } public function getInputShapeEnumValues(): array { return []; } public function getInputShapeDefaults(): array { return []; } public function getOptionalInputShapeEnumValues(): array { return []; } public function getOptionalInputShapeDefaults(): array { return []; } public function getOutputShapeEnumValues(): array { return []; } public function getOptionalOutputShapeEnumValues(): array { return []; }}
class ExternalTaskType implements ITaskType { public const ID = 'event:external:tasktype';
public function getId(): string { return self::ID; } public function getName(): string { return 'External Task Type via Event'; } public function getDescription(): string { return 'A task type added via event'; } public function getInputShape(): array { return ['external_input' => new ShapeDescriptor('Ext In', '', EShapeType::Text)]; } public function getOutputShape(): array { return ['external_output' => new ShapeDescriptor('Ext Out', '', EShapeType::Text)]; }}
class ConflictingExternalTaskType implements ITaskType { // Same ID as built-in TextToText
public const ID = TextToText::ID;
public function getId(): string { return self::ID; } public function getName(): string { return 'Conflicting External Task Type'; } public function getDescription(): string { return 'Overrides built-in TextToText'; } public function getInputShape(): array { return ['override_input' => new ShapeDescriptor('Override In', '', EShapeType::Number)]; } public function getOutputShape(): array { return ['override_output' => new ShapeDescriptor('Override Out', '', EShapeType::Number)]; }}
/** * @group DB */class TaskProcessingTest extends \Test\TestCase { private IManager $manager; private Coordinator $coordinator; private array $providers; private IServerContainer $serverContainer; private IEventDispatcher $eventDispatcher; private RegistrationContext $registrationContext; private TaskMapper $taskMapper; private IJobList $jobList; private IUserMountCache $userMountCache; private IRootFolder $rootFolder; private IConfig $config; private IAppConfig $appConfig;
public const TEST_USER = 'testuser';
protected function setUp(): void { parent::setUp();
$this->providers = [ SuccessfulSyncProvider::class => new SuccessfulSyncProvider(), FailingSyncProvider::class => new FailingSyncProvider(), BrokenSyncProvider::class => new BrokenSyncProvider(), AsyncProvider::class => new AsyncProvider(), AudioToImage::class => new AudioToImage(), SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(), FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(), SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(), FailingTextToImageProvider::class => new FailingTextToImageProvider(), ExternalProvider::class => new ExternalProvider(), ExternalTriggerableProvider::class => new ExternalTriggerableProvider(), ConflictingExternalProvider::class => new ConflictingExternalProvider(), ExternalTaskType::class => new ExternalTaskType(), ConflictingExternalTaskType::class => new ConflictingExternalTaskType(), ];
$userManager = Server::get(IUserManager::class); if (!$userManager->userExists(self::TEST_USER)) { $userManager->createUser(self::TEST_USER, 'test'); }
$this->serverContainer = $this->createMock(IServerContainer::class); $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) { return $this->providers[$class]; });
$this->eventDispatcher = new EventDispatcher( new \Symfony\Component\EventDispatcher\EventDispatcher(), $this->serverContainer, Server::get(LoggerInterface::class), );
$this->registrationContext = $this->createMock(RegistrationContext::class); $this->coordinator = $this->createMock(Coordinator::class); $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
$this->rootFolder = Server::get(IRootFolder::class);
$this->taskMapper = Server::get(TaskMapper::class);
$this->jobList = $this->createPartialMock(DummyJobList::class, ['add']); $this->jobList->expects($this->any())->method('add')->willReturnCallback(function (): void { });
$this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->configureEventDispatcherMock();
$text2imageManager = new \OC\TextToImage\Manager( $this->serverContainer, $this->coordinator, Server::get(LoggerInterface::class), $this->jobList, Server::get(\OC\TextToImage\Db\TaskMapper::class), Server::get(IConfig::class), Server::get(IAppDataFactory::class), );
$this->userMountCache = $this->createMock(IUserMountCache::class); $this->config = Server::get(IConfig::class); $this->appConfig = Server::get(IAppConfig::class); $this->manager = new Manager( $this->appConfig, $this->coordinator, $this->serverContainer, Server::get(LoggerInterface::class), $this->taskMapper, $this->jobList, $this->eventDispatcher, Server::get(IAppDataFactory::class), Server::get(IRootFolder::class), $text2imageManager, $this->userMountCache, Server::get(IClientService::class), Server::get(IAppManager::class), $userManager, Server::get(IUserSession::class), Server::get(ICacheFactory::class), Server::get(IFactory::class), ); }
private function getFile(string $name, string $content): File { $folder = $this->rootFolder->getUserFolder(self::TEST_USER); $file = $folder->newFile($name, $content); return $file; }
public function testShouldNotHaveAnyProviders(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); self::assertCount(0, $this->manager->getAvailableTaskTypes()); self::assertCount(0, $this->manager->getAvailableTaskTypeIds()); self::assertFalse($this->manager->hasProviders()); self::expectException(PreConditionNotMetException::class); $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null)); }
public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulSyncProvider::class) ]); $taskProcessingTypeSettings = [ TextToText::ID => false, ]; $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true); self::assertCount(0, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypes(true)); self::assertCount(0, $this->manager->getAvailableTaskTypeIds()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds(true)); self::assertTrue($this->manager->hasProviders()); self::expectException(PreConditionNotMetException::class); $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null)); }
public function testProviderShouldBeRegisteredAndTaskFailValidation(): void { $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', BrokenSyncProvider::class) ]); self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null); self::assertNull($task->getId()); self::expectException(ValidationException::class); $this->manager->scheduleTask($task); }
public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ new ServiceRegistration('test', AudioToImage::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', AsyncProvider::class) ]); $user = $this->createMock(IUser::class); $user->expects($this->any())->method('getUID')->willReturn(null); $mount = $this->createMock(ICachedMountInfo::class); $mount->expects($this->any())->method('getUser')->willReturn($user); $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue($this->manager->hasProviders());
$audioId = $this->getFile('audioInput', 'Hello')->getId(); $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); self::expectException(UnauthorizedException::class); $this->manager->scheduleTask($task); }
public function testProviderShouldBeRegisteredAndFail(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', FailingSyncProvider::class) ]); self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); $this->manager->scheduleTask($task); self::assertNotNull($task->getId()); self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage()); }
public function testProviderShouldBeRegisteredAndFailOutputValidation(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', BrokenSyncProvider::class) ]); self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); $this->manager->scheduleTask($task); self::assertNotNull($task->getId()); self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); self::assertEquals('The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec', $task->getErrorMessage()); }
public function testProviderShouldBeRegisteredAndRun(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulSyncProvider::class) ]); self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); $taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]]; self::assertTrue(isset($taskTypeStruct['inputShape']['input'])); self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType()); self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey'])); self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType()); self::assertTrue(isset($taskTypeStruct['outputShape']['output'])); self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType()); self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey'])); self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType());
self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); $this->manager->scheduleTask($task); self::assertNotNull($task->getId()); self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
// Task object retrieved from db is up-to-date
$task2 = $this->manager->getTask($task->getId()); self::assertEquals($task->getId(), $task2->getId()); self::assertEquals(['input' => 'Hello'], $task2->getInput()); self::assertNull($task2->getOutput()); self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage()); self::assertEquals(['output' => 'Hello'], $task->getOutput()); self::assertEquals(1, $task->getProgress()); }
public function testTaskTypeExplicitlyEnabled(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulSyncProvider::class) ]);
$taskProcessingTypeSettings = [ TextToText::ID => true, ]; $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); $this->manager->scheduleTask($task); self::assertNotNull($task->getId()); self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage()); self::assertEquals(['output' => 'Hello'], $task->getOutput()); self::assertEquals(1, $task->getProgress()); }
public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ new ServiceRegistration('test', AudioToImage::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', AsyncProvider::class) ]);
$user = $this->createMock(IUser::class); $user->expects($this->any())->method('getUID')->willReturn('testuser'); $mount = $this->createMock(ICachedMountInfo::class); $mount->expects($this->any())->method('getUser')->willReturn($user); $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
self::assertTrue($this->manager->hasProviders()); $audioId = $this->getFile('audioInput', 'Hello')->getId(); $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser'); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); $this->manager->scheduleTask($task); self::assertNotNull($task->getId()); self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
// Task object retrieved from db is up-to-date
$task2 = $this->manager->getTask($task->getId()); self::assertEquals($task->getId(), $task2->getId()); self::assertEquals(['audio' => $audioId], $task2->getInput()); self::assertNull($task2->getOutput()); self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$this->manager->setTaskProgress($task2->getId(), 0.1); $input = $this->manager->prepareInputData($task2); self::assertTrue(isset($input['audio'])); self::assertInstanceOf(File::class, $input['audio']); self::assertEquals($audioId, $input['audio']->getId());
$this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); self::assertEquals(1, $task->getProgress()); self::assertTrue(isset($task->getOutput()['spectrogram'])); $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); self::assertNotNull($node); self::assertInstanceOf(File::class, $node); self::assertEquals('World', $node->getContent()); }
public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds(): void { $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ new ServiceRegistration('test', AudioToImage::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', AsyncProvider::class) ]); $user = $this->createMock(IUser::class); $user->expects($this->any())->method('getUID')->willReturn('testuser'); $mount = $this->createMock(ICachedMountInfo::class); $mount->expects($this->any())->method('getUser')->willReturn($user); $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]); self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
self::assertTrue($this->manager->hasProviders()); $audioId = $this->getFile('audioInput', 'Hello')->getId(); $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser'); self::assertNull($task->getId()); self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); $this->manager->scheduleTask($task); self::assertNotNull($task->getId()); self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
// Task object retrieved from db is up-to-date
$task2 = $this->manager->getTask($task->getId()); self::assertEquals($task->getId(), $task2->getId()); self::assertEquals(['audio' => $audioId], $task2->getInput()); self::assertNull($task2->getOutput()); self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$this->manager->setTaskProgress($task2->getId(), 0.1); $input = $this->manager->prepareInputData($task2); self::assertTrue(isset($input['audio'])); self::assertInstanceOf(File::class, $input['audio']); self::assertEquals($audioId, $input['audio']->getId());
$outputFileId = $this->getFile('audioOutput', 'World')->getId();
$this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); self::assertEquals(1, $task->getProgress()); self::assertTrue(isset($task->getOutput()['spectrogram'])); $node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']); self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']); self::assertInstanceOf(File::class, $node); self::assertEquals('World', $node->getContent()); }
public function testNonexistentTask(): void { $this->expectException(NotFoundException::class); $this->manager->getTask(2147483646); }
public function testOldTasksShouldBeCleanedUp(): void { $currentTime = new \DateTime('now'); $timeFactory = $this->createMock(ITimeFactory::class); $timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime); $timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp());
$this->taskMapper = new TaskMapper( Server::get(IDBConnection::class), $timeFactory, );
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulSyncProvider::class) ]); self::assertCount(1, $this->manager->getAvailableTaskTypes()); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); $this->manager->scheduleTask($task);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId());
$currentTime = $currentTime->add(new \DateInterval('P1Y')); // run background job
$bgJob = new RemoveOldTasksBackgroundJob( $timeFactory, $this->manager, $this->taskMapper, Server::get(LoggerInterface::class), Server::get(IAppDataFactory::class), ); $bgJob->setArgument([]); $bgJob->start($this->jobList);
$this->expectException(NotFoundException::class); $this->manager->getTask($task->getId()); }
public function testShouldTransparentlyHandleTextProcessingProviders(): void { $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ ]); $taskTypes = $this->manager->getAvailableTaskTypes(); self::assertCount(1, $taskTypes); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue(isset($taskTypes[TextToTextSummary::ID])); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null); $this->manager->scheduleTask($task);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); self::assertIsArray($task->getOutput()); self::assertTrue(isset($task->getOutput()['output'])); self::assertEquals('Hello Summarize', $task->getOutput()['output']); self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran); }
public function testShouldTransparentlyHandleFailingTextProcessingProviders(): void { $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ ]); $taskTypes = $this->manager->getAvailableTaskTypes(); self::assertCount(1, $taskTypes); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue(isset($taskTypes[TextToTextSummary::ID])); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null); $this->manager->scheduleTask($task);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); self::assertTrue($task->getOutput() === null); self::assertEquals('ERROR', $task->getErrorMessage()); self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran); }
public function testShouldTransparentlyHandleText2ImageProviders(): void { $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([ new ServiceRegistration('test', SuccessfulTextToImageProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ ]); $taskTypes = $this->manager->getAvailableTaskTypes(); self::assertCount(1, $taskTypes); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue(isset($taskTypes[TextToImage::ID])); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null); $this->manager->scheduleTask($task);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); self::assertIsArray($task->getOutput()); self::assertTrue(isset($task->getOutput()['images'])); self::assertIsArray($task->getOutput()['images']); self::assertCount(3, $task->getOutput()['images']); self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran); $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); self::assertNotNull($node); self::assertInstanceOf(File::class, $node); self::assertEquals('test', $node->getContent()); }
public function testShouldTransparentlyHandleFailingText2ImageProviders(): void { $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([ new ServiceRegistration('test', FailingTextToImageProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ ]); $taskTypes = $this->manager->getAvailableTaskTypes(); self::assertCount(1, $taskTypes); self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); self::assertTrue(isset($taskTypes[TextToImage::ID])); self::assertTrue($this->manager->hasProviders()); $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null); $this->manager->scheduleTask($task);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
$backgroundJob = new SynchronousBackgroundJob( Server::get(ITimeFactory::class), $this->manager, $this->jobList, Server::get(LoggerInterface::class), ); $backgroundJob->start($this->jobList);
$task = $this->manager->getTask($task->getId()); self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); self::assertTrue($task->getOutput() === null); self::assertEquals('ERROR', $task->getErrorMessage()); self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran); }
public function testMergeProvidersLocalAndEvent() { // Arrange: Local provider registered, DIFFERENT external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulSyncProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$externalProvider = new ExternalProvider(); // ID = 'event:external:provider'
$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); $this->manager = $this->createManagerInstance();
// Act
$providers = $this->manager->getProviders();
// Assert: Both providers should be present
self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers); self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]); self::assertArrayHasKey(ExternalProvider::ID, $providers); self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]); self::assertCount(2, $providers); }
public function testGetProvidersIncludesExternalViaEvent() { // Arrange: No local providers, one external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$externalProvider = new ExternalProvider(); $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); $this->manager = $this->createManagerInstance(); // Create manager with configured mocks
// Act
$providers = $this->manager->getProviders(); // Returns ID-indexed array
// Assert
self::assertArrayHasKey(ExternalProvider::ID, $providers); self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]); self::assertCount(1, $providers); self::assertTrue($this->manager->hasProviders()); }
public function testGetAvailableTaskTypesIncludesExternalViaEvent() { // Arrange: No local types/providers, one external type and provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$externalProvider = new ExternalProvider(); // Provides ExternalTaskType
$externalTaskType = new ExternalTaskType(); $this->configureEventDispatcherMock( providersToAdd: [$externalProvider], taskTypesToAdd: [$externalTaskType] ); $this->manager = $this->createManagerInstance();
// Act
$availableTypes = $this->manager->getAvailableTaskTypes();
// Assert
self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes); self::assertContains(ExternalTaskType::ID, $this->manager->getAvailableTaskTypeIds()); self::assertEquals(ExternalTaskType::ID, $externalProvider->getTaskTypeId(), 'Test Sanity: Provider must handle the Task Type'); self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']); // Check if shapes match the external type/provider
self::assertArrayHasKey('external_input', $availableTypes[ExternalTaskType::ID]['inputShape']); self::assertArrayHasKey('external_output', $availableTypes[ExternalTaskType::ID]['outputShape']); self::assertEmpty($availableTypes[ExternalTaskType::ID]['optionalInputShape']); // From ExternalProvider
}
public function testLocalProviderWinsConflictWithEvent() { // Arrange: Local provider registered, conflicting external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', SuccessfulSyncProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$conflictingExternalProvider = new ConflictingExternalProvider(); // ID = 'test:sync:success'
$this->configureEventDispatcherMock(providersToAdd: [$conflictingExternalProvider]); $this->manager = $this->createManagerInstance();
// Act
$providers = $this->manager->getProviders();
// Assert: Only the local provider should be present for the conflicting ID
self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers); self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]); self::assertCount(1, $providers); // Ensure no extra provider was added
}
public function testTriggerableProviderWithNoOtherRunningTasks() { // Arrange: Local provider registered, conflicting external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']); $externalProvider->expects($this->once())->method('trigger'); $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); $this->manager = $this->createManagerInstance();
// Act
$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', 'foobar'); $this->manager->scheduleTask($task); }
public function testTriggerableProviderWithOtherRunningTasks() { // Arrange: Local provider registered, conflicting external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']); $externalProvider->expects($this->once())->method('trigger'); $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); $this->manager = $this->createManagerInstance();
$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', 'foobar'); $this->manager->scheduleTask($task); $this->manager->lockTask($task);
// Act
$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', 'foobar'); $this->manager->scheduleTask($task); }
public function testMergeTaskTypesLocalAndEvent() { // Arrange: Local type registered, DIFFERENT external type via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ new ServiceRegistration('test', AsyncProvider::class) ]); $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ new ServiceRegistration('test', AudioToImage::class) ]); $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
$externalTaskType = new ExternalTaskType(); // ID = 'event:external:tasktype'
$externalProvider = new ExternalProvider(); // Handles 'event:external:tasktype'
$this->configureEventDispatcherMock( providersToAdd: [$externalProvider], taskTypesToAdd: [$externalTaskType] ); $this->manager = $this->createManagerInstance();
// Act
$availableTypes = $this->manager->getAvailableTaskTypes(); $availableTypeIds = $this->manager->getAvailableTaskTypeIds();
// Assert: Both task types should be available
self::assertContains(AudioToImage::ID, $availableTypeIds); self::assertArrayHasKey(AudioToImage::ID, $availableTypes); self::assertEquals(AudioToImage::class, $availableTypes[AudioToImage::ID]['name']);
self::assertContains(ExternalTaskType::ID, $availableTypeIds); self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes); self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
self::assertCount(2, $availableTypes); }
private function createManagerInstance(): Manager { // Clear potentially cached config values if needed
$this->appConfig->deleteKey('core', 'ai.taskprocessing_type_preferences');
// Re-create Text2ImageManager if its state matters or mocks change
$text2imageManager = new \OC\TextToImage\Manager( $this->serverContainer, $this->coordinator, Server::get(LoggerInterface::class), $this->jobList, Server::get(\OC\TextToImage\Db\TaskMapper::class), $this->config, // Use the shared config mock
Server::get(IAppDataFactory::class), );
return new Manager( $this->appConfig, $this->coordinator, $this->serverContainer, Server::get(LoggerInterface::class), $this->taskMapper, $this->jobList, $this->eventDispatcher, // Use the potentially reconfigured mock
Server::get(IAppDataFactory::class), $this->rootFolder, $text2imageManager, $this->userMountCache, Server::get(IClientService::class), Server::get(IAppManager::class), Server::get(IUserManager::class), Server::get(IUserSession::class), Server::get(ICacheFactory::class), Server::get(IFactory::class), ); }
private function configureEventDispatcherMock( array $providersToAdd = [], array $taskTypesToAdd = [], ?int $expectedCalls = null, ): void { $dispatchExpectation = $expectedCalls === null ? $this->any() : $this->exactly($expectedCalls);
$this->eventDispatcher->expects($dispatchExpectation) ->method('dispatchTyped') ->willReturnCallback(function (object $event) use ($providersToAdd, $taskTypesToAdd): void { if ($event instanceof GetTaskProcessingProvidersEvent) { foreach ($providersToAdd as $providerInstance) { $event->addProvider($providerInstance); } foreach ($taskTypesToAdd as $taskTypeInstance) { $event->addTaskType($taskTypeInstance); } } }); }}
|