2024-04-29 16:21:07 +02:00
< ? php
/**
2024-05-21 21:44:34 +02:00
* SPDX - FileCopyrightText : 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2024-04-29 16:21:07 +02:00
*/
2024-08-30 10:00:50 +02:00
namespace Test\TaskProcessing ;
2024-04-29 16:21:07 +02:00
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 ;
2024-04-30 16:30:24 +02:00
use OC\TaskProcessing\RemoveOldTasksBackgroundJob ;
2024-07-17 12:35:13 +02:00
use OCP\App\IAppManager ;
2024-04-29 16:21:07 +02:00
use OCP\AppFramework\Utility\ITimeFactory ;
use OCP\BackgroundJob\IJobList ;
use OCP\EventDispatcher\IEventDispatcher ;
use OCP\Files\AppData\IAppDataFactory ;
2024-07-13 12:13:32 +02:00
use OCP\Files\Config\ICachedMountInfo ;
use OCP\Files\Config\IUserMountCache ;
2024-04-29 16:21:07 +02:00
use OCP\Files\IRootFolder ;
2024-07-17 12:35:13 +02:00
use OCP\Http\Client\IClientService ;
2025-01-23 10:31:29 +01:00
use OCP\ICacheFactory ;
2024-04-29 16:21:07 +02:00
use OCP\IConfig ;
2024-04-30 16:30:24 +02:00
use OCP\IDBConnection ;
2024-04-29 16:21:07 +02:00
use OCP\IServerContainer ;
2024-07-13 12:13:32 +02:00
use OCP\IUser ;
2024-05-10 07:18:14 +02:00
use OCP\IUserManager ;
2024-04-29 16:21:07 +02:00
use OCP\TaskProcessing\EShapeType ;
2025-04-08 20:45:37 +03:00
use OCP\TaskProcessing\Events\GetTaskProcessingProvidersEvent ;
2024-04-29 16:21:07 +02:00
use OCP\TaskProcessing\Events\TaskFailedEvent ;
use OCP\TaskProcessing\Events\TaskSuccessfulEvent ;
2024-04-30 16:30:24 +02:00
use OCP\TaskProcessing\Exception\NotFoundException ;
2024-04-29 16:21:07 +02:00
use OCP\TaskProcessing\Exception\ProcessingException ;
2024-05-06 10:22:59 +02:00
use OCP\TaskProcessing\Exception\UnauthorizedException ;
2024-04-29 16:21:07 +02:00
use OCP\TaskProcessing\Exception\ValidationException ;
use OCP\TaskProcessing\IManager ;
use OCP\TaskProcessing\IProvider ;
use OCP\TaskProcessing\ISynchronousProvider ;
use OCP\TaskProcessing\ITaskType ;
use OCP\TaskProcessing\ShapeDescriptor ;
use OCP\TaskProcessing\Task ;
2024-05-03 14:15:03 +02:00
use OCP\TaskProcessing\TaskTypes\TextToImage ;
2024-04-29 16:21:07 +02:00
use OCP\TaskProcessing\TaskTypes\TextToText ;
2024-05-03 14:15:03 +02:00
use OCP\TaskProcessing\TaskTypes\TextToTextSummary ;
use OCP\TextProcessing\SummaryTaskType ;
2024-04-29 16:21:07 +02:00
use PHPUnit\Framework\Constraint\IsInstanceOf ;
use Psr\Log\LoggerInterface ;
use Test\BackgroundJob\DummyJobList ;
class AudioToImage implements ITaskType {
2024-04-30 15:48:00 +02:00
public const ID = 'test:audiotoimage' ;
2024-04-29 16:21:07 +02:00
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 ;
}
2024-05-03 12:23:59 +02:00
public function getTaskTypeId () : string {
2024-04-29 16:21:07 +02:00
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 ),
];
}
2024-07-24 15:34:51 +02:00
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 [];
}
2024-04-29 16:21:07 +02:00
}
class SuccessfulSyncProvider implements IProvider , ISynchronousProvider {
2025-04-08 20:45:37 +03:00
public const ID = 'test:sync:success' ;
2024-04-29 16:21:07 +02:00
public function getId () : string {
2025-04-08 20:45:37 +03:00
return self :: ID ;
2024-04-29 16:21:07 +02:00
}
public function getName () : string {
return self :: class ;
}
2024-05-03 12:23:59 +02:00
public function getTaskTypeId () : string {
2024-04-29 16:21:07 +02:00
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 ),
];
}
2024-05-07 15:45:34 +02:00
public function process ( ? string $userId , array $input , callable $reportProgress ) : array {
2024-04-29 16:21:07 +02:00
return [ 'output' => $input [ 'input' ]];
}
2024-07-24 15:34:51 +02:00
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 [];
}
2024-04-29 16:21:07 +02:00
}
2024-12-11 18:12:56 +01:00
2024-04-29 16:21:07 +02:00
class FailingSyncProvider implements IProvider , ISynchronousProvider {
2024-04-30 15:48:00 +02:00
public const ERROR_MESSAGE = 'Failure' ;
2024-04-29 16:21:07 +02:00
public function getId () : string {
return 'test:sync:fail' ;
}
public function getName () : string {
return self :: class ;
}
2024-05-03 12:23:59 +02:00
public function getTaskTypeId () : string {
2024-04-29 16:21:07 +02:00
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 ),
];
}
2024-05-07 15:45:34 +02:00
public function process ( ? string $userId , array $input , callable $reportProgress ) : array {
2024-04-29 16:21:07 +02:00
throw new ProcessingException ( self :: ERROR_MESSAGE );
}
2024-07-24 15:34:51 +02:00
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 [];
}
2024-04-29 16:21:07 +02:00
}
class BrokenSyncProvider implements IProvider , ISynchronousProvider {
public function getId () : string {
return 'test:sync:broken-output' ;
}
public function getName () : string {
return self :: class ;
}
2024-05-03 12:23:59 +02:00
public function getTaskTypeId () : string {
2024-04-29 16:21:07 +02:00
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 ),
];
}
2024-05-07 15:45:34 +02:00
public function process ( ? string $userId , array $input , callable $reportProgress ) : array {
2024-04-29 16:21:07 +02:00
return [];
}
2024-07-24 15:34:51 +02:00
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 [];
}
2024-04-29 16:21:07 +02:00
}
2024-05-03 14:15:03 +02:00
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 ;
}
}
2025-04-08 20:45:37 +03:00
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 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 )];
}
}
2024-04-29 16:21:07 +02:00
/**
* @ 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 ;
2024-07-13 12:13:32 +02:00
private IUserMountCache $userMountCache ;
2024-05-10 06:51:41 +02:00
private IRootFolder $rootFolder ;
2024-12-11 18:12:56 +01:00
private IConfig $config ;
2024-05-06 11:57:05 +02:00
2024-05-10 07:34:15 +02:00
public const TEST_USER = 'testuser' ;
2024-05-10 07:18:14 +02:00
2024-04-29 16:21:07 +02:00
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 (),
2024-05-03 14:15:03 +02:00
SuccessfulTextProcessingSummaryProvider :: class => new SuccessfulTextProcessingSummaryProvider (),
FailingTextProcessingSummaryProvider :: class => new FailingTextProcessingSummaryProvider (),
SuccessfulTextToImageProvider :: class => new SuccessfulTextToImageProvider (),
FailingTextToImageProvider :: class => new FailingTextToImageProvider (),
2025-04-08 20:45:37 +03:00
ExternalProvider :: class => new ExternalProvider (),
ConflictingExternalProvider :: class => new ConflictingExternalProvider (),
ExternalTaskType :: class => new ExternalTaskType (),
ConflictingExternalTaskType :: class => new ConflictingExternalTaskType (),
2024-04-29 16:21:07 +02:00
];
2024-05-10 07:18:14 +02:00
$userManager = \OCP\Server :: get ( IUserManager :: class );
if ( ! $userManager -> userExists ( self :: TEST_USER )) {
$userManager -> createUser ( self :: TEST_USER , 'test' );
}
2024-04-29 16:21:07 +02:00
$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 ,
\OC :: $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 );
2024-05-10 06:51:41 +02:00
$this -> rootFolder = \OCP\Server :: get ( IRootFolder :: class );
2024-04-29 16:21:07 +02:00
$this -> taskMapper = \OCP\Server :: get ( TaskMapper :: class );
$this -> jobList = $this -> createPartialMock ( DummyJobList :: class , [ 'add' ]);
$this -> jobList -> expects ( $this -> any ()) -> method ( 'add' ) -> willReturnCallback ( function () {
});
2024-04-30 15:48:00 +02:00
$this -> eventDispatcher = $this -> createMock ( IEventDispatcher :: class );
2025-04-08 20:45:37 +03:00
$this -> configureEventDispatcherMock ();
2024-04-29 16:21:07 +02:00
2024-05-03 14:15:03 +02:00
$text2imageManager = new \OC\TextToImage\Manager (
$this -> serverContainer ,
$this -> coordinator ,
\OC :: $server -> get ( LoggerInterface :: class ),
$this -> jobList ,
\OC :: $server -> get ( \OC\TextToImage\Db\TaskMapper :: class ),
\OC :: $server -> get ( IConfig :: class ),
\OC :: $server -> get ( IAppDataFactory :: class ),
);
2024-07-13 12:13:32 +02:00
$this -> userMountCache = $this -> createMock ( IUserMountCache :: class );
2024-12-11 18:12:56 +01:00
$this -> config = \OC :: $server -> get ( IConfig :: class );
2024-04-29 16:21:07 +02:00
$this -> manager = new Manager (
2024-12-11 18:12:56 +01:00
$this -> config ,
2024-04-29 16:21:07 +02:00
$this -> coordinator ,
$this -> serverContainer ,
\OC :: $server -> get ( LoggerInterface :: class ),
$this -> taskMapper ,
$this -> jobList ,
$this -> eventDispatcher ,
\OC :: $server -> get ( IAppDataFactory :: class ),
\OC :: $server -> get ( IRootFolder :: class ),
2024-05-03 14:15:03 +02:00
$text2imageManager ,
2024-07-13 12:13:32 +02:00
$this -> userMountCache ,
2024-07-17 12:35:13 +02:00
\OC :: $server -> get ( IClientService :: class ),
\OC :: $server -> get ( IAppManager :: class ),
2025-01-23 10:31:29 +01:00
\OC :: $server -> get ( ICacheFactory :: class ),
2024-04-29 16:21:07 +02:00
);
}
private function getFile ( string $name , string $content ) : \OCP\Files\File {
2024-05-10 07:18:14 +02:00
$folder = $this -> rootFolder -> getUserFolder ( self :: TEST_USER );
2024-04-29 16:21:07 +02:00
$file = $folder -> newFile ( $name , $content );
2024-05-10 07:18:14 +02:00
return $file ;
2024-04-29 16:21:07 +02:00
}
public function testShouldNotHaveAnyProviders () : void {
$this -> registrationContext -> expects ( $this -> any ()) -> method ( 'getTaskProcessingProviders' ) -> willReturn ([]);
self :: assertCount ( 0 , $this -> manager -> getAvailableTaskTypes ());
self :: assertFalse ( $this -> manager -> hasProviders ());
2024-05-06 10:22:59 +02:00
self :: expectException ( \OCP\TaskProcessing\Exception\PreConditionNotMetException :: class );
2024-04-29 16:21:07 +02:00
$this -> manager -> scheduleTask ( new Task ( TextToText :: ID , [ 'input' => 'Hello' ], 'test' , null ));
}
2024-12-11 18:12:56 +01:00
public function testProviderShouldBeRegisteredAndTaskTypeDisabled () : void {
$this -> registrationContext -> expects ( $this -> any ()) -> method ( 'getTaskProcessingProviders' ) -> willReturn ([
new ServiceRegistration ( 'test' , SuccessfulSyncProvider :: class )
]);
$taskProcessingTypeSettings = [
TextToText :: ID => false ,
];
$this -> config -> setAppValue ( 'core' , 'ai.taskprocessing_type_preferences' , json_encode ( $taskProcessingTypeSettings ));
self :: assertCount ( 0 , $this -> manager -> getAvailableTaskTypes ());
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ( true ));
self :: assertTrue ( $this -> manager -> hasProviders ());
self :: expectException ( \OCP\TaskProcessing\Exception\PreConditionNotMetException :: class );
$this -> manager -> scheduleTask ( new Task ( TextToText :: ID , [ 'input' => 'Hello' ], 'test' , null ));
}
2024-04-29 16:21:07 +02:00
public function testProviderShouldBeRegisteredAndTaskFailValidation () : void {
2024-12-11 18:12:56 +01:00
$this -> config -> setAppValue ( 'core' , 'ai.taskprocessing_type_preferences' , '' );
2024-04-29 16:21:07 +02:00
$this -> registrationContext -> expects ( $this -> any ()) -> method ( 'getTaskProcessingProviders' ) -> willReturn ([
new ServiceRegistration ( 'test' , BrokenSyncProvider :: class )
]);
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
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 );
}
2024-05-06 10:22:59 +02:00
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 )
]);
2024-07-13 12:13:32 +02:00
$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 ]);
2024-05-06 10:22:59 +02:00
2024-07-13 12:13:32 +02:00
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
2024-05-06 10:22:59 +02:00
self :: assertTrue ( $this -> manager -> hasProviders ());
2024-07-13 12:13:32 +02:00
2024-05-06 10:22:59 +02:00
$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 );
}
2024-04-29 16:21:07 +02:00
public function testProviderShouldBeRegisteredAndFail () : void {
$this -> registrationContext -> expects ( $this -> any ()) -> method ( 'getTaskProcessingProviders' ) -> willReturn ([
new ServiceRegistration ( 'test' , FailingSyncProvider :: class )
]);
2024-04-30 16:30:24 +02:00
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
self :: assertTrue ( $this -> manager -> hasProviders ());
2024-04-29 16:21:07 +02:00
$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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 )
]);
2024-04-30 16:30:24 +02:00
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
self :: assertTrue ( $this -> manager -> hasProviders ());
2024-04-29 16:21:07 +02:00
$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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 )
]);
2024-04-30 16:30:24 +02:00
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
2024-04-29 16:21:07 +02:00
$taskTypeStruct = $this -> manager -> getAvailableTaskTypes ()[ array_keys ( $this -> manager -> getAvailableTaskTypes ())[ 0 ]];
2024-04-30 16:30:24 +02:00
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 ());
2024-04-29 16:21:07 +02:00
$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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 ());
2024-12-12 11:06:09 +01:00
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 -> config -> setAppValue ( 'core' , 'ai.taskprocessing_type_preferences' , json_encode ( $taskProcessingTypeSettings ));
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 ());
2024-04-29 16:21:07 +02:00
self :: assertEquals ([ 'output' => 'Hello' ], $task -> getOutput ());
self :: assertEquals ( 1 , $task -> getProgress ());
}
2024-07-13 12:13:32 +02:00
public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData () : void {
2024-04-29 16:21:07 +02:00
$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 )
]);
2024-07-13 12:13:32 +02:00
$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 ]);
2024-04-30 16:30:24 +02:00
self :: assertCount ( 1 , $this -> manager -> getAvailableTaskTypes ());
2024-04-29 16:21:07 +02:00
2024-04-30 16:30:24 +02:00
self :: assertTrue ( $this -> manager -> hasProviders ());
2024-04-29 16:21:07 +02:00
$audioId = $this -> getFile ( 'audioInput' , 'Hello' ) -> getId ();
2024-05-06 10:22:59 +02:00
$task = new Task ( AudioToImage :: ID , [ 'audio' => $audioId ], 'test' , 'testuser' );
2024-04-29 16:21:07 +02:00
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' ]));
2024-04-30 16:30:24 +02:00
self :: assertInstanceOf ( \OCP\Files\File :: class , $input [ 'audio' ]);
self :: assertEquals ( $audioId , $input [ 'audio' ] -> getId ());
2024-04-29 16:21:07 +02:00
2024-04-30 16:30:24 +02:00
$this -> manager -> setTaskResult ( $task2 -> getId (), null , [ 'spectrogram' => 'World' ]);
2024-04-29 16:21:07 +02:00
$task = $this -> manager -> getTask ( $task -> getId ());
self :: assertEquals ( Task :: STATUS_SUCCESSFUL , $task -> getStatus ());
self :: assertEquals ( 1 , $task -> getProgress ());
self :: assertTrue ( isset ( $task -> getOutput ()[ 'spectrogram' ]));
2024-05-10 06:51:41 +02:00
$node = $this -> rootFolder -> getFirstNodeByIdInPath ( $task -> getOutput ()[ 'spectrogram' ], '/' . $this -> rootFolder -> getAppDataDirectoryName () . '/' );
2024-04-29 16:21:07 +02:00
self :: assertNotNull ( $node );
self :: assertInstanceOf ( \OCP\Files\File :: class , $node );
self :: assertEquals ( 'World' , $node -> getContent ());
}
2024-07-13 12:13:22 +02:00
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 :: 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 ( \OCP\Files\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 ( \OCP\Files\File :: class , $node );
self :: assertEquals ( 'World' , $node -> getContent ());
}
2024-04-29 16:21:07 +02:00
public function testNonexistentTask () : void {
$this -> expectException ( \OCP\TaskProcessing\Exception\NotFoundException :: class );
$this -> manager -> getTask ( 2147483646 );
}
2024-04-30 16:30:24 +02:00
public function testOldTasksShouldBeCleanedUp () : void {
$currentTime = new \DateTime ( 'now' );
$timeFactory = $this -> createMock ( ITimeFactory :: class );
2024-04-30 17:01:41 +02:00
$timeFactory -> expects ( $this -> any ()) -> method ( 'getDateTime' ) -> willReturnCallback ( fn () => $currentTime );
$timeFactory -> expects ( $this -> any ()) -> method ( 'getTime' ) -> willReturnCallback ( fn () => $currentTime -> getTimestamp ());
2024-04-30 16:30:24 +02:00
$this -> taskMapper = new TaskMapper (
\OCP\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 :: 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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 -> taskMapper ,
\OC :: $server -> get ( LoggerInterface :: class ),
2024-05-06 10:22:59 +02:00
\OCP\Server :: get ( IAppDataFactory :: class ),
2024-04-30 16:30:24 +02:00
);
$bgJob -> setArgument ([]);
$bgJob -> start ( $this -> jobList );
$this -> expectException ( NotFoundException :: class );
$this -> manager -> getTask ( $task -> getId ());
}
2024-05-03 14:15:03 +02:00
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 :: 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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 :: 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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 :: 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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 );
2024-05-10 06:51:41 +02:00
$node = $this -> rootFolder -> getFirstNodeByIdInPath ( $task -> getOutput ()[ 'images' ][ 0 ], '/' . $this -> rootFolder -> getAppDataDirectoryName () . '/' );
self :: assertNotNull ( $node );
self :: assertInstanceOf ( \OCP\Files\File :: class , $node );
self :: assertEquals ( 'test' , $node -> getContent ());
2024-05-03 14:15:03 +02:00
}
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 :: 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 \OC\TaskProcessing\SynchronousBackgroundJob (
\OCP\Server :: get ( ITimeFactory :: class ),
$this -> manager ,
$this -> jobList ,
\OCP\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 );
}
2025-04-08 20:45:37 +03:00
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 :: 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 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 ();
// Assert: Both task types should be available
self :: assertArrayHasKey ( AudioToImage :: ID , $availableTypes );
self :: assertEquals ( AudioToImage :: class , $availableTypes [ AudioToImage :: ID ][ 'name' ]);
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 -> config -> deleteAppValue ( 'core' , 'ai.taskprocessing_type_preferences' );
// Re-create Text2ImageManager if its state matters or mocks change
$text2imageManager = new \OC\TextToImage\Manager (
$this -> serverContainer ,
$this -> coordinator ,
\OC :: $server -> get ( LoggerInterface :: class ),
$this -> jobList ,
\OC :: $server -> get ( \OC\TextToImage\Db\TaskMapper :: class ),
$this -> config , // Use the shared config mock
\OC :: $server -> get ( IAppDataFactory :: class ),
);
return new Manager (
$this -> config ,
$this -> coordinator ,
$this -> serverContainer ,
\OC :: $server -> get ( LoggerInterface :: class ),
$this -> taskMapper ,
$this -> jobList ,
$this -> eventDispatcher , // Use the potentially reconfigured mock
\OC :: $server -> get ( IAppDataFactory :: class ),
$this -> rootFolder ,
$text2imageManager ,
$this -> userMountCache ,
\OC :: $server -> get ( IClientService :: class ),
\OC :: $server -> get ( IAppManager :: class ),
\OC :: $server -> get ( ICacheFactory :: 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 ) {
if ( $event instanceof GetTaskProcessingProvidersEvent ) {
foreach ( $providersToAdd as $providerInstance ) {
$event -> addProvider ( $providerInstance );
}
foreach ( $taskTypesToAdd as $taskTypeInstance ) {
$event -> addTaskType ( $taskTypeInstance );
}
}
});
}
2024-04-29 16:21:07 +02:00
}