TaskProcessingTest.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace Test\TextProcessing;
  7. use OC\AppFramework\Bootstrap\Coordinator;
  8. use OC\AppFramework\Bootstrap\RegistrationContext;
  9. use OC\AppFramework\Bootstrap\ServiceRegistration;
  10. use OC\EventDispatcher\EventDispatcher;
  11. use OC\TaskProcessing\Db\TaskMapper;
  12. use OC\TaskProcessing\Manager;
  13. use OC\TaskProcessing\RemoveOldTasksBackgroundJob;
  14. use OCP\App\IAppManager;
  15. use OCP\AppFramework\Utility\ITimeFactory;
  16. use OCP\BackgroundJob\IJobList;
  17. use OCP\EventDispatcher\IEventDispatcher;
  18. use OCP\Files\AppData\IAppDataFactory;
  19. use OCP\Files\Config\ICachedMountInfo;
  20. use OCP\Files\Config\IUserMountCache;
  21. use OCP\Files\IRootFolder;
  22. use OCP\Http\Client\IClientService;
  23. use OCP\IConfig;
  24. use OCP\IDBConnection;
  25. use OCP\IServerContainer;
  26. use OCP\IUser;
  27. use OCP\IUserManager;
  28. use OCP\SpeechToText\ISpeechToTextManager;
  29. use OCP\TaskProcessing\EShapeType;
  30. use OCP\TaskProcessing\Events\TaskFailedEvent;
  31. use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
  32. use OCP\TaskProcessing\Exception\NotFoundException;
  33. use OCP\TaskProcessing\Exception\ProcessingException;
  34. use OCP\TaskProcessing\Exception\UnauthorizedException;
  35. use OCP\TaskProcessing\Exception\ValidationException;
  36. use OCP\TaskProcessing\IManager;
  37. use OCP\TaskProcessing\IProvider;
  38. use OCP\TaskProcessing\ISynchronousProvider;
  39. use OCP\TaskProcessing\ITaskType;
  40. use OCP\TaskProcessing\ShapeDescriptor;
  41. use OCP\TaskProcessing\Task;
  42. use OCP\TaskProcessing\TaskTypes\TextToImage;
  43. use OCP\TaskProcessing\TaskTypes\TextToText;
  44. use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
  45. use OCP\TextProcessing\SummaryTaskType;
  46. use PHPUnit\Framework\Constraint\IsInstanceOf;
  47. use Psr\Log\LoggerInterface;
  48. use Test\BackgroundJob\DummyJobList;
  49. class AudioToImage implements ITaskType {
  50. public const ID = 'test:audiotoimage';
  51. public function getId(): string {
  52. return self::ID;
  53. }
  54. public function getName(): string {
  55. return self::class;
  56. }
  57. public function getDescription(): string {
  58. return self::class;
  59. }
  60. public function getInputShape(): array {
  61. return [
  62. 'audio' => new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio),
  63. ];
  64. }
  65. public function getOutputShape(): array {
  66. return [
  67. 'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image),
  68. ];
  69. }
  70. }
  71. class AsyncProvider implements IProvider {
  72. public function getId(): string {
  73. return 'test:sync:success';
  74. }
  75. public function getName(): string {
  76. return self::class;
  77. }
  78. public function getTaskTypeId(): string {
  79. return AudioToImage::ID;
  80. }
  81. public function getExpectedRuntime(): int {
  82. return 10;
  83. }
  84. public function getOptionalInputShape(): array {
  85. return [
  86. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  87. ];
  88. }
  89. public function getOptionalOutputShape(): array {
  90. return [
  91. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  92. ];
  93. }
  94. public function getInputShapeEnumValues(): array {
  95. return [];
  96. }
  97. public function getInputShapeDefaults(): array {
  98. return [];
  99. }
  100. public function getOptionalInputShapeEnumValues(): array {
  101. return [];
  102. }
  103. public function getOptionalInputShapeDefaults(): array {
  104. return [];
  105. }
  106. public function getOutputShapeEnumValues(): array {
  107. return [];
  108. }
  109. public function getOptionalOutputShapeEnumValues(): array {
  110. return [];
  111. }
  112. }
  113. class SuccessfulSyncProvider implements IProvider, ISynchronousProvider {
  114. public function getId(): string {
  115. return 'test:sync:success';
  116. }
  117. public function getName(): string {
  118. return self::class;
  119. }
  120. public function getTaskTypeId(): string {
  121. return TextToText::ID;
  122. }
  123. public function getExpectedRuntime(): int {
  124. return 10;
  125. }
  126. public function getOptionalInputShape(): array {
  127. return [
  128. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  129. ];
  130. }
  131. public function getOptionalOutputShape(): array {
  132. return [
  133. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  134. ];
  135. }
  136. public function process(?string $userId, array $input, callable $reportProgress): array {
  137. return ['output' => $input['input']];
  138. }
  139. public function getInputShapeEnumValues(): array {
  140. return [];
  141. }
  142. public function getInputShapeDefaults(): array {
  143. return [];
  144. }
  145. public function getOptionalInputShapeEnumValues(): array {
  146. return [];
  147. }
  148. public function getOptionalInputShapeDefaults(): array {
  149. return [];
  150. }
  151. public function getOutputShapeEnumValues(): array {
  152. return [];
  153. }
  154. public function getOptionalOutputShapeEnumValues(): array {
  155. return [];
  156. }
  157. }
  158. class FailingSyncProvider implements IProvider, ISynchronousProvider {
  159. public const ERROR_MESSAGE = 'Failure';
  160. public function getId(): string {
  161. return 'test:sync:fail';
  162. }
  163. public function getName(): string {
  164. return self::class;
  165. }
  166. public function getTaskTypeId(): string {
  167. return TextToText::ID;
  168. }
  169. public function getExpectedRuntime(): int {
  170. return 10;
  171. }
  172. public function getOptionalInputShape(): array {
  173. return [
  174. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  175. ];
  176. }
  177. public function getOptionalOutputShape(): array {
  178. return [
  179. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  180. ];
  181. }
  182. public function process(?string $userId, array $input, callable $reportProgress): array {
  183. throw new ProcessingException(self::ERROR_MESSAGE);
  184. }
  185. public function getInputShapeEnumValues(): array {
  186. return [];
  187. }
  188. public function getInputShapeDefaults(): array {
  189. return [];
  190. }
  191. public function getOptionalInputShapeEnumValues(): array {
  192. return [];
  193. }
  194. public function getOptionalInputShapeDefaults(): array {
  195. return [];
  196. }
  197. public function getOutputShapeEnumValues(): array {
  198. return [];
  199. }
  200. public function getOptionalOutputShapeEnumValues(): array {
  201. return [];
  202. }
  203. }
  204. class BrokenSyncProvider implements IProvider, ISynchronousProvider {
  205. public function getId(): string {
  206. return 'test:sync:broken-output';
  207. }
  208. public function getName(): string {
  209. return self::class;
  210. }
  211. public function getTaskTypeId(): string {
  212. return TextToText::ID;
  213. }
  214. public function getExpectedRuntime(): int {
  215. return 10;
  216. }
  217. public function getOptionalInputShape(): array {
  218. return [
  219. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  220. ];
  221. }
  222. public function getOptionalOutputShape(): array {
  223. return [
  224. 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
  225. ];
  226. }
  227. public function process(?string $userId, array $input, callable $reportProgress): array {
  228. return [];
  229. }
  230. public function getInputShapeEnumValues(): array {
  231. return [];
  232. }
  233. public function getInputShapeDefaults(): array {
  234. return [];
  235. }
  236. public function getOptionalInputShapeEnumValues(): array {
  237. return [];
  238. }
  239. public function getOptionalInputShapeDefaults(): array {
  240. return [];
  241. }
  242. public function getOutputShapeEnumValues(): array {
  243. return [];
  244. }
  245. public function getOptionalOutputShapeEnumValues(): array {
  246. return [];
  247. }
  248. }
  249. class SuccessfulTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {
  250. public bool $ran = false;
  251. public function getName(): string {
  252. return 'TEST Vanilla LLM Provider';
  253. }
  254. public function process(string $prompt): string {
  255. $this->ran = true;
  256. return $prompt . ' Summarize';
  257. }
  258. public function getTaskType(): string {
  259. return SummaryTaskType::class;
  260. }
  261. }
  262. class FailingTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {
  263. public bool $ran = false;
  264. public function getName(): string {
  265. return 'TEST Vanilla LLM Provider';
  266. }
  267. public function process(string $prompt): string {
  268. $this->ran = true;
  269. throw new \Exception('ERROR');
  270. }
  271. public function getTaskType(): string {
  272. return SummaryTaskType::class;
  273. }
  274. }
  275. class SuccessfulTextToImageProvider implements \OCP\TextToImage\IProvider {
  276. public bool $ran = false;
  277. public function getId(): string {
  278. return 'test:successful';
  279. }
  280. public function getName(): string {
  281. return 'TEST Provider';
  282. }
  283. public function generate(string $prompt, array $resources): void {
  284. $this->ran = true;
  285. foreach($resources as $resource) {
  286. fwrite($resource, 'test');
  287. }
  288. }
  289. public function getExpectedRuntime(): int {
  290. return 1;
  291. }
  292. }
  293. class FailingTextToImageProvider implements \OCP\TextToImage\IProvider {
  294. public bool $ran = false;
  295. public function getId(): string {
  296. return 'test:failing';
  297. }
  298. public function getName(): string {
  299. return 'TEST Provider';
  300. }
  301. public function generate(string $prompt, array $resources): void {
  302. $this->ran = true;
  303. throw new \RuntimeException('ERROR');
  304. }
  305. public function getExpectedRuntime(): int {
  306. return 1;
  307. }
  308. }
  309. /**
  310. * @group DB
  311. */
  312. class TaskProcessingTest extends \Test\TestCase {
  313. private IManager $manager;
  314. private Coordinator $coordinator;
  315. private array $providers;
  316. private IServerContainer $serverContainer;
  317. private IEventDispatcher $eventDispatcher;
  318. private RegistrationContext $registrationContext;
  319. private TaskMapper $taskMapper;
  320. private IJobList $jobList;
  321. private IUserMountCache $userMountCache;
  322. private IRootFolder $rootFolder;
  323. public const TEST_USER = 'testuser';
  324. protected function setUp(): void {
  325. parent::setUp();
  326. $this->providers = [
  327. SuccessfulSyncProvider::class => new SuccessfulSyncProvider(),
  328. FailingSyncProvider::class => new FailingSyncProvider(),
  329. BrokenSyncProvider::class => new BrokenSyncProvider(),
  330. AsyncProvider::class => new AsyncProvider(),
  331. AudioToImage::class => new AudioToImage(),
  332. SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(),
  333. FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(),
  334. SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(),
  335. FailingTextToImageProvider::class => new FailingTextToImageProvider(),
  336. ];
  337. $userManager = \OCP\Server::get(IUserManager::class);
  338. if (!$userManager->userExists(self::TEST_USER)) {
  339. $userManager->createUser(self::TEST_USER, 'test');
  340. }
  341. $this->serverContainer = $this->createMock(IServerContainer::class);
  342. $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) {
  343. return $this->providers[$class];
  344. });
  345. $this->eventDispatcher = new EventDispatcher(
  346. new \Symfony\Component\EventDispatcher\EventDispatcher(),
  347. $this->serverContainer,
  348. \OC::$server->get(LoggerInterface::class),
  349. );
  350. $this->registrationContext = $this->createMock(RegistrationContext::class);
  351. $this->coordinator = $this->createMock(Coordinator::class);
  352. $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
  353. $this->rootFolder = \OCP\Server::get(IRootFolder::class);
  354. $this->taskMapper = \OCP\Server::get(TaskMapper::class);
  355. $this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
  356. $this->jobList->expects($this->any())->method('add')->willReturnCallback(function () {
  357. });
  358. $config = $this->createMock(IConfig::class);
  359. $config->method('getAppValue')
  360. ->with('core', 'ai.textprocessing_provider_preferences', '')
  361. ->willReturn('');
  362. $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
  363. $textProcessingManager = new \OC\TextProcessing\Manager(
  364. $this->serverContainer,
  365. $this->coordinator,
  366. \OC::$server->get(LoggerInterface::class),
  367. $this->jobList,
  368. \OC::$server->get(\OC\TextProcessing\Db\TaskMapper::class),
  369. \OC::$server->get(IConfig::class),
  370. );
  371. $text2imageManager = new \OC\TextToImage\Manager(
  372. $this->serverContainer,
  373. $this->coordinator,
  374. \OC::$server->get(LoggerInterface::class),
  375. $this->jobList,
  376. \OC::$server->get(\OC\TextToImage\Db\TaskMapper::class),
  377. \OC::$server->get(IConfig::class),
  378. \OC::$server->get(IAppDataFactory::class),
  379. );
  380. $this->userMountCache = $this->createMock(IUserMountCache::class);
  381. $this->manager = new Manager(
  382. \OC::$server->get(IConfig::class),
  383. $this->coordinator,
  384. $this->serverContainer,
  385. \OC::$server->get(LoggerInterface::class),
  386. $this->taskMapper,
  387. $this->jobList,
  388. $this->eventDispatcher,
  389. \OC::$server->get(IAppDataFactory::class),
  390. \OC::$server->get(IRootFolder::class),
  391. $textProcessingManager,
  392. $text2imageManager,
  393. \OC::$server->get(ISpeechToTextManager::class),
  394. $this->userMountCache,
  395. \OC::$server->get(IClientService::class),
  396. \OC::$server->get(IAppManager::class),
  397. );
  398. }
  399. private function getFile(string $name, string $content): \OCP\Files\File {
  400. $folder = $this->rootFolder->getUserFolder(self::TEST_USER);
  401. $file = $folder->newFile($name, $content);
  402. return $file;
  403. }
  404. public function testShouldNotHaveAnyProviders() {
  405. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
  406. self::assertCount(0, $this->manager->getAvailableTaskTypes());
  407. self::assertFalse($this->manager->hasProviders());
  408. self::expectException(\OCP\TaskProcessing\Exception\PreConditionNotMetException::class);
  409. $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
  410. }
  411. public function testProviderShouldBeRegisteredAndTaskFailValidation() {
  412. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  413. new ServiceRegistration('test', BrokenSyncProvider::class)
  414. ]);
  415. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  416. self::assertTrue($this->manager->hasProviders());
  417. $task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null);
  418. self::assertNull($task->getId());
  419. self::expectException(ValidationException::class);
  420. $this->manager->scheduleTask($task);
  421. }
  422. public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation() {
  423. $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
  424. new ServiceRegistration('test', AudioToImage::class)
  425. ]);
  426. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  427. new ServiceRegistration('test', AsyncProvider::class)
  428. ]);
  429. $user = $this->createMock(IUser::class);
  430. $user->expects($this->any())->method('getUID')->willReturn(null);
  431. $mount = $this->createMock(ICachedMountInfo::class);
  432. $mount->expects($this->any())->method('getUser')->willReturn($user);
  433. $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
  434. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  435. self::assertTrue($this->manager->hasProviders());
  436. $audioId = $this->getFile('audioInput', 'Hello')->getId();
  437. $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null);
  438. self::assertNull($task->getId());
  439. self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
  440. self::expectException(UnauthorizedException::class);
  441. $this->manager->scheduleTask($task);
  442. }
  443. public function testProviderShouldBeRegisteredAndFail() {
  444. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  445. new ServiceRegistration('test', FailingSyncProvider::class)
  446. ]);
  447. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  448. self::assertTrue($this->manager->hasProviders());
  449. $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
  450. self::assertNull($task->getId());
  451. self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
  452. $this->manager->scheduleTask($task);
  453. self::assertNotNull($task->getId());
  454. self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
  455. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
  456. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  457. \OCP\Server::get(ITimeFactory::class),
  458. $this->manager,
  459. $this->jobList,
  460. \OCP\Server::get(LoggerInterface::class),
  461. );
  462. $backgroundJob->start($this->jobList);
  463. $task = $this->manager->getTask($task->getId());
  464. self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
  465. self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage());
  466. }
  467. public function testProviderShouldBeRegisteredAndFailOutputValidation() {
  468. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  469. new ServiceRegistration('test', BrokenSyncProvider::class)
  470. ]);
  471. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  472. self::assertTrue($this->manager->hasProviders());
  473. $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
  474. self::assertNull($task->getId());
  475. self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
  476. $this->manager->scheduleTask($task);
  477. self::assertNotNull($task->getId());
  478. self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
  479. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
  480. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  481. \OCP\Server::get(ITimeFactory::class),
  482. $this->manager,
  483. $this->jobList,
  484. \OCP\Server::get(LoggerInterface::class),
  485. );
  486. $backgroundJob->start($this->jobList);
  487. $task = $this->manager->getTask($task->getId());
  488. self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
  489. 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());
  490. }
  491. public function testProviderShouldBeRegisteredAndRun() {
  492. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  493. new ServiceRegistration('test', SuccessfulSyncProvider::class)
  494. ]);
  495. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  496. $taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]];
  497. self::assertTrue(isset($taskTypeStruct['inputShape']['input']));
  498. self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType());
  499. self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey']));
  500. self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType());
  501. self::assertTrue(isset($taskTypeStruct['outputShape']['output']));
  502. self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType());
  503. self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey']));
  504. self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType());
  505. self::assertTrue($this->manager->hasProviders());
  506. $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
  507. self::assertNull($task->getId());
  508. self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
  509. $this->manager->scheduleTask($task);
  510. self::assertNotNull($task->getId());
  511. self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
  512. // Task object retrieved from db is up-to-date
  513. $task2 = $this->manager->getTask($task->getId());
  514. self::assertEquals($task->getId(), $task2->getId());
  515. self::assertEquals(['input' => 'Hello'], $task2->getInput());
  516. self::assertNull($task2->getOutput());
  517. self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
  518. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
  519. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  520. \OCP\Server::get(ITimeFactory::class),
  521. $this->manager,
  522. $this->jobList,
  523. \OCP\Server::get(LoggerInterface::class),
  524. );
  525. $backgroundJob->start($this->jobList);
  526. $task = $this->manager->getTask($task->getId());
  527. self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is '. $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
  528. self::assertEquals(['output' => 'Hello'], $task->getOutput());
  529. self::assertEquals(1, $task->getProgress());
  530. }
  531. public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData() {
  532. $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
  533. new ServiceRegistration('test', AudioToImage::class)
  534. ]);
  535. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  536. new ServiceRegistration('test', AsyncProvider::class)
  537. ]);
  538. $user = $this->createMock(IUser::class);
  539. $user->expects($this->any())->method('getUID')->willReturn('testuser');
  540. $mount = $this->createMock(ICachedMountInfo::class);
  541. $mount->expects($this->any())->method('getUser')->willReturn($user);
  542. $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
  543. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  544. self::assertTrue($this->manager->hasProviders());
  545. $audioId = $this->getFile('audioInput', 'Hello')->getId();
  546. $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
  547. self::assertNull($task->getId());
  548. self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
  549. $this->manager->scheduleTask($task);
  550. self::assertNotNull($task->getId());
  551. self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
  552. // Task object retrieved from db is up-to-date
  553. $task2 = $this->manager->getTask($task->getId());
  554. self::assertEquals($task->getId(), $task2->getId());
  555. self::assertEquals(['audio' => $audioId], $task2->getInput());
  556. self::assertNull($task2->getOutput());
  557. self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
  558. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
  559. $this->manager->setTaskProgress($task2->getId(), 0.1);
  560. $input = $this->manager->prepareInputData($task2);
  561. self::assertTrue(isset($input['audio']));
  562. self::assertInstanceOf(\OCP\Files\File::class, $input['audio']);
  563. self::assertEquals($audioId, $input['audio']->getId());
  564. $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']);
  565. $task = $this->manager->getTask($task->getId());
  566. self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
  567. self::assertEquals(1, $task->getProgress());
  568. self::assertTrue(isset($task->getOutput()['spectrogram']));
  569. $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
  570. self::assertNotNull($node);
  571. self::assertInstanceOf(\OCP\Files\File::class, $node);
  572. self::assertEquals('World', $node->getContent());
  573. }
  574. public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds() {
  575. $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
  576. new ServiceRegistration('test', AudioToImage::class)
  577. ]);
  578. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  579. new ServiceRegistration('test', AsyncProvider::class)
  580. ]);
  581. $user = $this->createMock(IUser::class);
  582. $user->expects($this->any())->method('getUID')->willReturn('testuser');
  583. $mount = $this->createMock(ICachedMountInfo::class);
  584. $mount->expects($this->any())->method('getUser')->willReturn($user);
  585. $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
  586. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  587. self::assertTrue($this->manager->hasProviders());
  588. $audioId = $this->getFile('audioInput', 'Hello')->getId();
  589. $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
  590. self::assertNull($task->getId());
  591. self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
  592. $this->manager->scheduleTask($task);
  593. self::assertNotNull($task->getId());
  594. self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
  595. // Task object retrieved from db is up-to-date
  596. $task2 = $this->manager->getTask($task->getId());
  597. self::assertEquals($task->getId(), $task2->getId());
  598. self::assertEquals(['audio' => $audioId], $task2->getInput());
  599. self::assertNull($task2->getOutput());
  600. self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
  601. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
  602. $this->manager->setTaskProgress($task2->getId(), 0.1);
  603. $input = $this->manager->prepareInputData($task2);
  604. self::assertTrue(isset($input['audio']));
  605. self::assertInstanceOf(\OCP\Files\File::class, $input['audio']);
  606. self::assertEquals($audioId, $input['audio']->getId());
  607. $outputFileId = $this->getFile('audioOutput', 'World')->getId();
  608. $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
  609. $task = $this->manager->getTask($task->getId());
  610. self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
  611. self::assertEquals(1, $task->getProgress());
  612. self::assertTrue(isset($task->getOutput()['spectrogram']));
  613. $node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']);
  614. self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']);
  615. self::assertInstanceOf(\OCP\Files\File::class, $node);
  616. self::assertEquals('World', $node->getContent());
  617. }
  618. public function testNonexistentTask() {
  619. $this->expectException(\OCP\TaskProcessing\Exception\NotFoundException::class);
  620. $this->manager->getTask(2147483646);
  621. }
  622. public function testOldTasksShouldBeCleanedUp() {
  623. $currentTime = new \DateTime('now');
  624. $timeFactory = $this->createMock(ITimeFactory::class);
  625. $timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime);
  626. $timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp());
  627. $this->taskMapper = new TaskMapper(
  628. \OCP\Server::get(IDBConnection::class),
  629. $timeFactory,
  630. );
  631. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  632. new ServiceRegistration('test', SuccessfulSyncProvider::class)
  633. ]);
  634. self::assertCount(1, $this->manager->getAvailableTaskTypes());
  635. self::assertTrue($this->manager->hasProviders());
  636. $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
  637. $this->manager->scheduleTask($task);
  638. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
  639. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  640. \OCP\Server::get(ITimeFactory::class),
  641. $this->manager,
  642. $this->jobList,
  643. \OCP\Server::get(LoggerInterface::class),
  644. );
  645. $backgroundJob->start($this->jobList);
  646. $task = $this->manager->getTask($task->getId());
  647. $currentTime = $currentTime->add(new \DateInterval('P1Y'));
  648. // run background job
  649. $bgJob = new RemoveOldTasksBackgroundJob(
  650. $timeFactory,
  651. $this->taskMapper,
  652. \OC::$server->get(LoggerInterface::class),
  653. \OCP\Server::get(IAppDataFactory::class),
  654. );
  655. $bgJob->setArgument([]);
  656. $bgJob->start($this->jobList);
  657. $this->expectException(NotFoundException::class);
  658. $this->manager->getTask($task->getId());
  659. }
  660. public function testShouldTransparentlyHandleTextProcessingProviders() {
  661. $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
  662. new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class)
  663. ]);
  664. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  665. ]);
  666. $taskTypes = $this->manager->getAvailableTaskTypes();
  667. self::assertCount(1, $taskTypes);
  668. self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
  669. self::assertTrue($this->manager->hasProviders());
  670. $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
  671. $this->manager->scheduleTask($task);
  672. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
  673. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  674. \OCP\Server::get(ITimeFactory::class),
  675. $this->manager,
  676. $this->jobList,
  677. \OCP\Server::get(LoggerInterface::class),
  678. );
  679. $backgroundJob->start($this->jobList);
  680. $task = $this->manager->getTask($task->getId());
  681. self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
  682. self::assertIsArray($task->getOutput());
  683. self::assertTrue(isset($task->getOutput()['output']));
  684. self::assertEquals('Hello Summarize', $task->getOutput()['output']);
  685. self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran);
  686. }
  687. public function testShouldTransparentlyHandleFailingTextProcessingProviders() {
  688. $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
  689. new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class)
  690. ]);
  691. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  692. ]);
  693. $taskTypes = $this->manager->getAvailableTaskTypes();
  694. self::assertCount(1, $taskTypes);
  695. self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
  696. self::assertTrue($this->manager->hasProviders());
  697. $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
  698. $this->manager->scheduleTask($task);
  699. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
  700. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  701. \OCP\Server::get(ITimeFactory::class),
  702. $this->manager,
  703. $this->jobList,
  704. \OCP\Server::get(LoggerInterface::class),
  705. );
  706. $backgroundJob->start($this->jobList);
  707. $task = $this->manager->getTask($task->getId());
  708. self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
  709. self::assertTrue($task->getOutput() === null);
  710. self::assertEquals('ERROR', $task->getErrorMessage());
  711. self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran);
  712. }
  713. public function testShouldTransparentlyHandleText2ImageProviders() {
  714. $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
  715. new ServiceRegistration('test', SuccessfulTextToImageProvider::class)
  716. ]);
  717. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  718. ]);
  719. $taskTypes = $this->manager->getAvailableTaskTypes();
  720. self::assertCount(1, $taskTypes);
  721. self::assertTrue(isset($taskTypes[TextToImage::ID]));
  722. self::assertTrue($this->manager->hasProviders());
  723. $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
  724. $this->manager->scheduleTask($task);
  725. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
  726. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  727. \OCP\Server::get(ITimeFactory::class),
  728. $this->manager,
  729. $this->jobList,
  730. \OCP\Server::get(LoggerInterface::class),
  731. );
  732. $backgroundJob->start($this->jobList);
  733. $task = $this->manager->getTask($task->getId());
  734. self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
  735. self::assertIsArray($task->getOutput());
  736. self::assertTrue(isset($task->getOutput()['images']));
  737. self::assertIsArray($task->getOutput()['images']);
  738. self::assertCount(3, $task->getOutput()['images']);
  739. self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran);
  740. $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
  741. self::assertNotNull($node);
  742. self::assertInstanceOf(\OCP\Files\File::class, $node);
  743. self::assertEquals('test', $node->getContent());
  744. }
  745. public function testShouldTransparentlyHandleFailingText2ImageProviders() {
  746. $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
  747. new ServiceRegistration('test', FailingTextToImageProvider::class)
  748. ]);
  749. $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
  750. ]);
  751. $taskTypes = $this->manager->getAvailableTaskTypes();
  752. self::assertCount(1, $taskTypes);
  753. self::assertTrue(isset($taskTypes[TextToImage::ID]));
  754. self::assertTrue($this->manager->hasProviders());
  755. $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
  756. $this->manager->scheduleTask($task);
  757. $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
  758. $backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
  759. \OCP\Server::get(ITimeFactory::class),
  760. $this->manager,
  761. $this->jobList,
  762. \OCP\Server::get(LoggerInterface::class),
  763. );
  764. $backgroundJob->start($this->jobList);
  765. $task = $this->manager->getTask($task->getId());
  766. self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
  767. self::assertTrue($task->getOutput() === null);
  768. self::assertEquals('ERROR', $task->getErrorMessage());
  769. self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran);
  770. }
  771. }