TaskProcessingTest.php 28 KB

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