diff --git a/src/FuzeWorks/Async/Constraint/DependencyConstraint.php b/src/FuzeWorks/Async/Constraint/DependencyConstraint.php index 426b1ee..ff78586 100644 --- a/src/FuzeWorks/Async/Constraint/DependencyConstraint.php +++ b/src/FuzeWorks/Async/Constraint/DependencyConstraint.php @@ -56,7 +56,7 @@ use FuzeWorks\Logger; class DependencyConstraint implements Constraint { - public $dependencies = []; + protected $dependencies = []; protected $delayTimes = 3; @@ -100,7 +100,6 @@ class DependencyConstraint implements Constraint // Get dependency $dependencyTask = $taskStorage->getTaskById($dependency); - // If the dependency task is completed, ignore it and continue to next dependency if ($dependencyTask->getStatus() === Task::COMPLETED) continue; @@ -146,6 +145,16 @@ class DependencyConstraint implements Constraint return time() + $this->delayTimes; } + /** + * Return a list of dependencies + * + * @return array + */ + public function getDependencies() + { + return $this->dependencies; + } + /** * Load the tasks library, so that dependencies can get scanned later * diff --git a/src/FuzeWorks/Async/Handler.php b/src/FuzeWorks/Async/Handler.php index f44c3db..6e3c570 100644 --- a/src/FuzeWorks/Async/Handler.php +++ b/src/FuzeWorks/Async/Handler.php @@ -38,6 +38,15 @@ namespace FuzeWorks\Async; interface Handler { + /** + * Gets invoked upon being added to the Task + * + * @param Task $task + * @return mixed + * @throws TasksException + */ + public function init(Task $task); + /** * Retrieve the parent handler that will first handle this task, before this child Handler * @@ -55,9 +64,9 @@ interface Handler /** * Import the parent output into the child * - * @param mixed $input + * @param string $input */ - public function setParentInput($input): void; + public function setParentInput(string $input): void; /** * The handler method used to handle this task. @@ -73,9 +82,9 @@ interface Handler /** * Any output generated by primaryHandler should be returned here. * - * @return mixed + * @return string */ - public function getOutput(); + public function getOutput(): string; /** * The handler method used after the primaryHandler if so requested @@ -90,8 +99,8 @@ interface Handler /** * Any output generated by postHandler should be returned here * - * @return mixed + * @return string */ - public function getPostOutput(); + public function getPostOutput(): string; } \ No newline at end of file diff --git a/src/FuzeWorks/Async/Handler/DependentTaskHandler.php b/src/FuzeWorks/Async/Handler/DependentTaskHandler.php new file mode 100644 index 0000000..e93e9fa --- /dev/null +++ b/src/FuzeWorks/Async/Handler/DependentTaskHandler.php @@ -0,0 +1,250 @@ +dependencyList = $dependencyList; + $this->delayTimes = $delayTimes; + } + + /** + * @inheritDoc + */ + public function init(Task $task) + { + if (!empty($this->dependencyList)) + $task->addConstraint(new DependencyConstraint($this->dependencyList, $this->delayTimes)); + } + + /** + * @inheritDoc + */ + public function primaryHandler(Task $task): bool + { + // First find all the dependencies + try { + $dependencies = $this->fetchDependencies($task); + $this->output = json_encode($dependencies); + return true; + } catch (TasksException $e) { + $this->output = 'Failed to fetch dependencies. TasksException: ' . $e->getMessage(); + return false; + } + } + + /** + * @inheritDoc + */ + public function getOutput(): string + { + return $this->output; + } + + /** + * @inheritDoc + */ + public function postHandler(Task $task) + { + // First find all the dependencies + try { + $dependencies = $this->fetchDependencies($task); + $this->output = json_encode($dependencies); + return true; + } catch (TasksException $e) { + $this->output = 'Failed to fetch dependencies. TasksException: ' . $e->getMessage(); + return false; + } + } + + /** + * @inheritDoc + */ + public function getPostOutput(): string + { + return $this->output; + } + + /** + * @inheritDoc + */ + public function getParentHandler(): ?Handler + { + return $this->parentHandler; + } + + /** + * @inheritDoc + */ + public function setParentHandler(Handler $parentHandler): void + { + $this->parentHandler = $parentHandler; + } + + /** + * @inheritDoc + */ + public function setParentInput(string $input): void + { + // Parent output gets set at this handler's output. + // Only if this class has something to intervene it will override the parent output + // Which should be always... but alas. + $this->output = $input; + } + + /** + * @param Task $task + * @return array + * @throws TasksException + */ + protected function fetchDependencies(Task $task): array + { + // When it receives the task, all dependencies should already be handled + // the primary handler will therefore connect with the DependencyConstraint and fetch dependencies + $constraints = $task->getConstraints(); + + // First prepare a list of dependencies + $dependencies = []; + $dependencyConstraints = []; + foreach ($constraints as $constraint) { + if ($constraint instanceof Constraint) + $dependencyConstraints[] = $constraint; + } + + // If no dependencies found, throw exception + if (empty($dependencyConstraints)) + return $dependencies; + + // Afterwards build a list of dependencies + /** @var DependencyConstraint $constraint */ + foreach ($dependencyConstraints as $constraint) { + foreach ($constraint->getDependencies() as $dependency) { + if (!isset($dependencies[$dependency])) + $dependencies[$dependency] = []; + } + } + + // Now that all dependencies are determined, fetch all the output + $tasks = $this->loadTasksLib(); + $taskStorage = $tasks->getTaskStorage(); + foreach ($dependencies as $dependency => $data) + { + // Fetch the task + try { + $dependencyTask = $taskStorage->getTaskById($dependency); + + // Then fetch all output + $dependencies[$dependency]['status'] = $dependencyTask->getStatus(); + $dependencies[$dependency]['output'] = $dependencyTask->getOutput(); + $dependencies[$dependency]['errors'] = $dependencyTask->getErrors(); + $dependencies[$dependency]['post'] = $dependencyTask->getPostOutput(); + $dependencies[$dependency]['postErrors'] = $dependencyTask->getPostErrors(); + } catch (TasksException $e) { + $dependencies[$dependency]['status'] = Task::FAILED; + $dependencies[$dependency]['output'] = null; + $dependencies[$dependency]['errors'] = 'Task not found.'; + $dependencies[$dependency]['post'] = null; + $dependencies[$dependency]['postErrors'] = null; + } + } + + return $dependencies; + } + + /** + * Load the tasks library, so that dependencies can get scanned later + * + * @return Tasks + * @throws TasksException + */ + private function loadTasksLib(): Tasks + { + try { + /** @var Libraries $libraries */ + $libraries = Factory::getInstance('libraries'); + + /** @var Tasks $tasks */ + $tasks = $libraries->get('async'); + + return $tasks; + } catch (FactoryException | LibraryException $e) { + throw new TasksException("Could not constrain task. Async library could not be loaded."); + } + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Async/Supervisors/ParallelSuperVisor.php b/src/FuzeWorks/Async/Supervisors/ParallelSuperVisor.php index 44d3c35..2d0425f 100644 --- a/src/FuzeWorks/Async/Supervisors/ParallelSuperVisor.php +++ b/src/FuzeWorks/Async/Supervisors/ParallelSuperVisor.php @@ -117,9 +117,8 @@ class ParallelSuperVisor implements SuperVisor // RUNNING: check if task is still running. If not, set result based on output elseif ($task->getStatus() === Task::RUNNING) { - // @todo Find a way to use the latest output $isRunning = $this->executor->getTaskRunning($task); - $output = $this->taskStorage->readTaskOutput($task, 1); + $output = $this->taskStorage->readTaskOutput($task); $hasOutput = !is_null($output); // If nothing is found, the process has crashed and status PFAILED should be set @@ -204,9 +203,8 @@ class ParallelSuperVisor implements SuperVisor // POST: when a task is currently running in it's postHandler elseif ($task->getStatus() === Task::POST) { - // @todo Find a way to use the latest output $isRunning = $this->executor->getTaskRunning($task); - $output = $this->taskStorage->readPostOutput($task, 1); + $output = $this->taskStorage->readPostOutput($task); $hasOutput = !is_null($output); // If a task is not running and has no output, an error has occurred diff --git a/src/FuzeWorks/Async/Task.php b/src/FuzeWorks/Async/Task.php index b8a3d15..be800ca 100644 --- a/src/FuzeWorks/Async/Task.php +++ b/src/FuzeWorks/Async/Task.php @@ -209,6 +209,9 @@ class Task throw new TasksException("Could not create Task. Provided arguments are not serializable."); $this->arguments = $args; + + // Init the handler + $this->handler->init($this); } /** diff --git a/src/FuzeWorks/Async/TaskStorage/DummyTaskStorage.php b/src/FuzeWorks/Async/TaskStorage/DummyTaskStorage.php index f0a47ca..d99aa2d 100644 --- a/src/FuzeWorks/Async/TaskStorage/DummyTaskStorage.php +++ b/src/FuzeWorks/Async/TaskStorage/DummyTaskStorage.php @@ -237,16 +237,19 @@ class DummyTaskStorage implements TaskStorage */ public function readTaskOutput(Task $task, int $attempt = 0): ?array { - if ($attempt !== 0) + if (!isset($this->taskOutput[$task->getId()]['task'])) + return null; + + if ($attempt === 0) + $attempt = count($this->taskOutput[$task->getId()]['task']); + + if ($attempt === -1) + return $this->taskOutput[$task->getId()]['task']; + else { if (isset($this->taskOutput[$task->getId()]['task'][$attempt])) return $this->taskOutput[$task->getId()]['task'][$attempt]; } - else - { - if (isset($this->taskOutput[$task->getId()]['task'])) - return $this->taskOutput[$task->getId()]['task']; - } return null; } @@ -256,16 +259,19 @@ class DummyTaskStorage implements TaskStorage */ public function readPostOutput(Task $task, int $attempt = 0): ?array { - if ($attempt !== 0) + if (!isset($this->taskOutput[$task->getId()]['post'])) + return null; + + if ($attempt === 0) + $attempt = count($this->taskOutput[$task->getId()]['post']); + + if ($attempt === -1) + return $this->taskOutput[$task->getId()]['post']; + else { if (isset($this->taskOutput[$task->getId()]['post'][$attempt])) return $this->taskOutput[$task->getId()]['post'][$attempt]; } - else - { - if (isset($this->taskOutput[$task->getId()]['post'])) - return $this->taskOutput[$task->getId()]['post']; - } return null; } diff --git a/src/FuzeWorks/Async/TaskStorage/RedisTaskStorage.php b/src/FuzeWorks/Async/TaskStorage/RedisTaskStorage.php index deab7c0..e63dd89 100644 --- a/src/FuzeWorks/Async/TaskStorage/RedisTaskStorage.php +++ b/src/FuzeWorks/Async/TaskStorage/RedisTaskStorage.php @@ -271,19 +271,12 @@ class RedisTaskStorage implements TaskStorage // First get the task ID $taskId = $task->getId(); - if ($attempt !== 0) - { - // Check if this output exists - if (!$this->conn->hExists($this->key_prefix . $taskId, 'output' . $attempt)) - return null; + // If a nothing in particular is requested, fetch the latest and select that as the attempt + if ($attempt === 0) + $attempt = $this->conn->hGet($this->key_prefix . $taskId, 'taskOutputAttempts'); - // Load and convert the data - $data = $this->conn->hGet($this->key_prefix . $taskId, 'output' . $attempt); - $data = unserialize($data); - - return ['output' => $data['output'], 'errors' => $data['errors'], 'statusCode' => $data['statusCode']]; - } - else + // If -1 is requested, fetch all + if ($attempt === -1) { // Get amount of attempts $totalAttempts = $this->conn->hGet($this->key_prefix . $taskId, 'taskOutputAttempts'); @@ -303,9 +296,22 @@ class RedisTaskStorage implements TaskStorage if (!empty($output)) return $output; - - return null; } + // If a specific one is requested, fetch that one + else + { + // Check if this output exists + if (!$this->conn->hExists($this->key_prefix . $taskId, 'output' . $attempt)) + return null; + + // Load and convert the data + $data = $this->conn->hGet($this->key_prefix . $taskId, 'output' . $attempt); + $data = unserialize($data); + + return ['output' => $data['output'], 'errors' => $data['errors'], 'statusCode' => $data['statusCode']]; + } + + return null; } /** @@ -316,19 +322,12 @@ class RedisTaskStorage implements TaskStorage // First get the task ID $taskId = $task->getId(); - if ($attempt !== 0) - { - // Check if this output exists - if (!$this->conn->hExists($this->key_prefix . $taskId, 'postOutput' . $attempt)) - return null; + // If a nothing in particular is requested, fetch the latest and select that as the attempt + if ($attempt === 0) + $attempt = $this->conn->hGet($this->key_prefix . $taskId, 'taskPostAttempts'); - // Load and convert the data - $data = $this->conn->hGet($this->key_prefix . $taskId, 'postOutput' . $attempt); - $data = unserialize($data); - - return ['output' => $data['output'], 'errors' => $data['errors'], 'statusCode' => $data['statusCode']]; - } - else + // If -1 is requested, fetch all + if ($attempt === -1) { // Get amount of attempts $totalAttempts = $this->conn->hGet($this->key_prefix . $taskId, 'taskPostAttempts'); @@ -348,9 +347,22 @@ class RedisTaskStorage implements TaskStorage if (!empty($output)) return $output; - - return null; } + // If a specific one is requested, fetch that one + else + { + // Check if this output exists + if (!$this->conn->hExists($this->key_prefix . $taskId, 'postOutput' . $attempt)) + return null; + + // Load and convert the data + $data = $this->conn->hGet($this->key_prefix . $taskId, 'postOutput' . $attempt); + $data = unserialize($data); + + return ['output' => $data['output'], 'errors' => $data['errors'], 'statusCode' => $data['statusCode']]; + } + + return null; } /** diff --git a/src/FuzeWorks/Async/Tasks.php b/src/FuzeWorks/Async/Tasks.php index 1247d2e..9875d12 100644 --- a/src/FuzeWorks/Async/Tasks.php +++ b/src/FuzeWorks/Async/Tasks.php @@ -94,11 +94,10 @@ class Tasks implements iLibrary } /** - * @param string $bootstrapFile * @return SuperVisor * @throws TasksException */ - public function getSuperVisor(string $bootstrapFile): SuperVisor + public function getSuperVisor(): SuperVisor { if (isset($this->supervisor)) return $this->supervisor; @@ -116,7 +115,7 @@ class Tasks implements iLibrary $parameters = isset($cfg[$type]['parameters']) && is_array($cfg[$type]['parameters']) ? $cfg[$type]['parameters'] : []; // Then add the TaskStorage and Executor to the parameters - array_unshift($parameters, $this->getTaskStorage(), $this->getExecutor($bootstrapFile)); + array_unshift($parameters, $this->getTaskStorage(), $this->getExecutor()); // If the type does not exist, throw an exception if (!class_exists($class, true)) @@ -183,11 +182,10 @@ class Tasks implements iLibrary /** * Fetch the Executor based on the configured type * - * @param string $bootstrapFile * @return Executor * @throws TasksException */ - protected function getExecutor(string $bootstrapFile): Executor + protected function getExecutor(): Executor { if (isset($this->executor)) return $this->executor; @@ -209,7 +207,7 @@ class Tasks implements iLibrary throw new TasksException("Could not get Executor. Type of '$class' not found."); // And load the Executor and test if everything is in order - $object = new $class($bootstrapFile, $parameters); + $object = new $class($parameters); if (!$object instanceof Executor) throw new TasksException("Could not get Executor. Type '$class' is not instanceof Executor."); diff --git a/test/base/DependenciesTest.php b/test/base/DependenciesTest.php new file mode 100644 index 0000000..6cd00d7 --- /dev/null +++ b/test/base/DependenciesTest.php @@ -0,0 +1,453 @@ +tasks = Factory::getInstance('libraries')->get('async'); + $this->taskStorage = $this->tasks->getTaskStorage(); + $this->taskStorage->reset(); + + // Reset events + Events::$listeners = []; + } + + /* ---------------------------------- Test the dependency constraint ------------------ */ + + public function testHasConstrainedDeps() + { + // Create the dependent tasks + $depTask1 = new Task('depTask1', new EmptyHandler()); + $depTask2 = new Task('depTask2', new EmptyHandler()); + + // Write those dependencies to TaskStorage + $this->taskStorage->addTask($depTask1); + $this->taskStorage->addTask($depTask2); + + // Create the constraint + $constraint = new DependencyConstraint(['depTask1', 'depTask2']); + + // And a dummyTask to accompany + $dummyTask = new Task('dependentTask', new EmptyHandler()); + $dummyTask->addConstraint($constraint); + + // Test that the constraint is the same + $this->assertSame([$constraint], $dummyTask->getConstraints()); + + // And test the intervention + $this->assertTrue($constraint->intervene($dummyTask)); + $this->assertEquals(Task::DELAYED, $constraint->blockCode()); + $this->assertEquals(time() + 3, $constraint->delayTime()); + } + + /** + * @depends testHasConstrainedDeps + */ + public function testDelayTimes() + { + // Create the dependent tasks + $depTask1 = new Task('depTask1', new EmptyHandler()); + $depTask2 = new Task('depTask2', new EmptyHandler()); + + // Write those dependencies to TaskStorage + $this->taskStorage->addTask($depTask1); + $this->taskStorage->addTask($depTask2); + + // Create some useless dummy task + $dummyTask = new Task('dependentTask', new EmptyHandler()); + + // Create the constraints + // Default time + $constraintDef = new DependencyConstraint(['depTask1', 'depTask2']); + + // Modified time (30) + $constraintMod1 = new DependencyConstraint(['depTask1', 'depTask2'], 30); + + // And another (60) + $constraintMod2 = new DependencyConstraint(['depTask1', 'depTask2'], 60); + + // And intervene all of them + $this->assertTrue($constraintDef->intervene($dummyTask)); + $this->assertTrue($constraintMod1->intervene($dummyTask)); + $this->assertTrue($constraintMod2->intervene($dummyTask)); + + // And check the results + $this->assertEquals(Task::DELAYED, $constraintDef->blockCode()); + $this->assertEquals(Task::DELAYED, $constraintMod1->blockCode()); + $this->assertEquals(Task::DELAYED, $constraintMod2->blockCode()); + $this->assertEquals(time() + 3, $constraintDef->delayTime()); + $this->assertEquals(time() + 30, $constraintMod1->delayTime()); + $this->assertEquals(time() + 60, $constraintMod2->delayTime()); + } + + public function testHasFailedDeps() + { + // Create the dependent tasks + $depTask1 = new Task('depTask1', new EmptyHandler()); + $depTask2 = new Task('depTask2', new EmptyHandler()); + + // And set the first as completed, and second as failed + $depTask1->setStatus(Task::COMPLETED); + $depTask2->setStatus(Task::CANCELLED); + + // Write those dependencies to TaskStorage + $this->taskStorage->addTask($depTask1); + $this->taskStorage->addTask($depTask2); + + // Create the constraint + $constraint = new DependencyConstraint(['depTask1', 'depTask2']); + + // And a dummyTask to accompany + $dummyTask = new Task('dependentTask', new EmptyHandler()); + $dummyTask->addConstraint($constraint); + + // Test that the constraint is the same + $this->assertSame([$constraint], $dummyTask->getConstraints()); + + // And test the intervention + $this->assertTrue($constraint->intervene($dummyTask)); + $this->assertEquals(Task::CANCELLED, $constraint->blockCode()); + $this->assertEquals('Task cancelled due to failed dependency.', $dummyTask->getErrors()); + } + + public function testHasCompletedDeps() + { + // Create the dependent tasks + $depTask1 = new Task('depTask1', new EmptyHandler()); + $depTask2 = new Task('depTask2', new EmptyHandler()); + + // And set the first as completed, and second as failed + $depTask1->setStatus(Task::COMPLETED); + $depTask2->setStatus(Task::COMPLETED); + + // Write those dependencies to TaskStorage + $this->taskStorage->addTask($depTask1); + $this->taskStorage->addTask($depTask2); + + // Create the constraint + $constraint = new DependencyConstraint(['depTask1', 'depTask2']); + + // And a dummyTask to accompany + $dummyTask = new Task('dependentTask', new EmptyHandler()); + $dummyTask->addConstraint($constraint); + + // Test that the constraint is the same + $this->assertSame([$constraint], $dummyTask->getConstraints()); + + // And test the intervention + $this->assertFalse($constraint->intervene($dummyTask)); + } + + public function testGetDependencies() + { + $constraint = new DependencyConstraint(['someTask1', 'someTask2']); + $this->assertEquals(['someTask1', 'someTask2'], $constraint->getDependencies()); + } + + /* ---------------------------------- Test the dependent task handler ----------------- */ + + public function testAddedDependencies() + { + $handler = new DependentTaskHandler(['someTask1', 'someTask2']); + $dummyTask = new Task('someTask', $handler); + + // Check that the constraints match expectations + /** @var DependencyConstraint[] $constraints */ + $constraints = $dummyTask->getConstraints(); + $this->assertInstanceOf(DependencyConstraint::class, $constraints[0]); + + // And that the dependencies match + $this->assertEquals(['someTask1', 'someTask2'], $constraints[0]->getDependencies()); + } + + public function testPassingOutput() + { + // Create the dependent tasks + $depTask1 = new Task('someTask', new EmptyHandler()); + $depTask2 = new Task('someTask2', new EmptyHandler()); + + // Give the dependencies some output + $depTask1->setOutput('First Output', ''); + $depTask2->setOutput('Second Output', ''); + + // Write those to TaskStorage + $this->taskStorage->addTask($depTask1); + $this->taskStorage->addTask($depTask2); + + // Create the task + $handler = new DependentTaskHandler(['someTask', 'someTask2']); + + // Create a dummy Task + $dummyTask = new Task('someTask3', $handler); + + // Assert that all is well + $this->assertTrue($handler->primaryHandler($dummyTask)); + + // And test the handler's output + $this->assertEquals(json_encode([ + 'someTask' => [ + 'status' => Task::PENDING, + 'output' => 'First Output', + 'errors' => '', + 'post' => null, + 'postErrors' => null + ], + 'someTask2' => [ + 'status' => Task::PENDING, + 'output' => 'Second Output', + 'errors' => '', + 'post' => null, + 'postErrors' => null + ] + ]), $handler->getOutput()); + + // And test the post handler + $this->assertTrue($handler->postHandler($dummyTask)); + $this->assertEquals(json_encode([ + 'someTask' => [ + 'status' => Task::PENDING, + 'output' => 'First Output', + 'errors' => '', + 'post' => null, + 'postErrors' => null + ], + 'someTask2' => [ + 'status' => Task::PENDING, + 'output' => 'Second Output', + 'errors' => '', + 'post' => null, + 'postErrors' => null + ] + ]), $handler->getPostOutput()); + } + + /** + * @depends testPassingOutput + */ + public function testMissingDependency() + { + // Create the task + $handler = new DependentTaskHandler(['someTask']); + + // Create a dummy Task + $dummyTask = new Task('someTask2', $handler); + + // Assert that all is well + $this->assertTrue($handler->primaryHandler($dummyTask)); + + // And test the handler's output + $this->assertEquals(json_encode([ + 'someTask' => [ + 'status' => Task::FAILED, + 'output' => null, + 'errors' => 'Task not found.', + 'post' => null, + 'postErrors' => null + ], + ]), $handler->getOutput()); + + // And test the post handler + $this->assertTrue($handler->postHandler($dummyTask)); + $this->assertEquals(json_encode([ + 'someTask' => [ + 'status' => Task::FAILED, + 'output' => null, + 'errors' => 'Task not found.', + 'post' => null, + 'postErrors' => null + ], + ]), $handler->getPostOutput()); + } + + /** + * @depends testPassingOutput + */ + public function testNoDepedencies() + { + // Create the task + $handler = new DependentTaskHandler([]); + + // Create a dummy Task + $dummyTask = new Task('someTask', $handler); + + // Assert that all is well + $this->assertTrue($handler->primaryHandler($dummyTask)); + $this->assertEquals(json_encode([]), $handler->getOutput()); + + // And test the post handler + $this->assertTrue($handler->postHandler($dummyTask)); + $this->assertEquals(json_encode([]), $handler->getPostOutput()); + } + + public function testParentHandler() + { + // Test pass output + $handler = new DependentTaskHandler([]); + $handler->setParentInput('Passed Input'); + $this->assertEquals('Passed Input', $handler->getOutput()); + + // Test passing a handler + $handler = new DependentTaskHandler([]); + $parentHandler = $this->createMock(Handler::class); + $handler->setParentHandler($parentHandler); + $this->assertSame($parentHandler, $handler->getParentHandler()); + } + + public function testPassDependencyOutput() + { + // Build all systems for this test + $superVisor = $this->tasks->getSuperVisor(); + + // Create the dependency + $dependency = new Task('dependency', new ArgumentedHandler(0, 'Prepared Output')); + + // Write the task to TaskStorage + $this->taskStorage->addTask($dependency); + + // Now create the dependent task + $dependent = new Task('dependent', new DependentTaskHandler(['dependency'], 2)); + + // And write that task to TaskStorage + $this->taskStorage->addTask($dependent); + + // Now we make the SuperVisor cycle, to start the dependency and set the dependent to WAIT + $this->assertEquals(SuperVisor::RUNNING, $superVisor->cycle()); + + // Assert that everything is running + $this->assertEquals(Task::RUNNING, + $this->taskStorage->getTaskById($dependency->getId())->getStatus() + ); + $this->assertEquals(Task::DELAYED, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + + // Give the task some time to finish + usleep(500000); + + // And re-run the SuperVisor + $this->assertEquals(SuperVisor::RUNNING, $superVisor->cycle()); + + // Now check the tasks again. Dependency should be finished and have output, + // whereas dependent should still be delayed + $this->assertEquals(Task::SUCCESS, + $this->taskStorage->getTaskById($dependency->getId())->getStatus() + ); + $this->assertEquals(Task::DELAYED, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + + // Cycle again and see the dependency be completed, and dependent still delayed + $this->assertEquals(SuperVisor::RUNNING, $superVisor->cycle()); + $this->assertEquals(Task::COMPLETED, + $this->taskStorage->getTaskById($dependency->getId())->getStatus() + ); + $this->assertEquals(Task::DELAYED, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + + // Also check that output is correct + $this->assertEquals('Prepared Output', + $this->taskStorage->getTaskById($dependency->getId())->getOutput() + ); + + // Now wait long enough for the delay to be finished + usleep(2500000); + + // Now cycle again, and expect the task to be pending + $this->assertEquals(SuperVisor::RUNNING, $superVisor->cycle()); + $this->assertEquals(Task::PENDING, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + + // Cycle again and expect it to be running + $this->assertEquals(SuperVisor::RUNNING, $superVisor->cycle()); + $this->assertEquals(Task::RUNNING, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + + // Give the task some time to finish + usleep(500000); + + // And cycle again and expect the task to have succeeded + $this->assertEquals(SuperVisor::RUNNING, $superVisor->cycle()); + $this->assertEquals(Task::SUCCESS, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + + // Cycle again and expect the task to have completed, and test its output + $this->assertEquals(SuperVisor::FINISHED, $superVisor->cycle()); + $this->assertEquals(Task::COMPLETED, + $this->taskStorage->getTaskById($dependent->getId())->getStatus() + ); + $this->assertEquals( + json_encode(['dependency' => [ + 'status' => Task::COMPLETED, + 'output' => 'Prepared Output', + 'errors' => '', + 'post' => null, + 'postErrors' => null + ]]), + $this->taskStorage->getTaskById($dependent->getId())->getOutput() + ); + } +} diff --git a/test/base/TaskStorageTest.php b/test/base/TaskStorageTest.php index dd54e7d..7bc3502 100644 --- a/test/base/TaskStorageTest.php +++ b/test/base/TaskStorageTest.php @@ -338,11 +338,14 @@ class TaskStorageTest extends TestCase $this->assertTrue($this->taskStorage->writeTaskOutput($dummyTask, 'output2', 'errors2', 0)); // Then try to read all the output - $output = $this->taskStorage->readTaskOutput($dummyTask); + $output = $this->taskStorage->readTaskOutput($dummyTask, -1); $this->assertEquals([ ['output' => 'output1', 'errors' => 'errors1', 'statusCode' => 0], ['output' => 'output2', 'errors' => 'errors2', 'statusCode' => 0] ], $output); + + // Then try and read the latest + $this->assertEquals(['output' => 'output2', 'errors' => 'errors2', 'statusCode' => 0], $this->taskStorage->readTaskOutput($dummyTask)); } /** @@ -392,11 +395,16 @@ class TaskStorageTest extends TestCase // Attempt to load the default output $output = $this->taskStorage->readTaskOutput($dummyTask); + $this->assertEquals('output2', $output['output']); + $this->assertEquals('errors2', $output['errors']); + $this->assertEquals(102, $output['statusCode']); + + // And to load all $this->assertEquals([ ['output' => 'output0', 'errors' => 'errors0', 'statusCode' => 100], ['output' => 'output1', 'errors' => 'errors1', 'statusCode' => 101], ['output' => 'output2', 'errors' => 'errors2', 'statusCode' => 102] - ], $output); + ], $this->taskStorage->readTaskOutput($dummyTask, -1)); } /** @@ -446,11 +454,13 @@ class TaskStorageTest extends TestCase $this->assertTrue($this->taskStorage->writePostOutput($dummyTask, 'output2', 'errors2', 0)); // Then try to read all the output - $output = $this->taskStorage->readPostOutput($dummyTask); + $output = $this->taskStorage->readPostOutput($dummyTask, -1); $this->assertEquals([ ['output' => 'output1', 'errors' => 'errors1', 'statusCode' => 0], ['output' => 'output2', 'errors' => 'errors2', 'statusCode' => 0] ], $output); + + $this->assertEquals(['output' => 'output2', 'errors' => 'errors2', 'statusCode' => 0], $this->taskStorage->readPostOutput($dummyTask)); } /** @@ -488,11 +498,16 @@ class TaskStorageTest extends TestCase // Attempt to load the default output $output = $this->taskStorage->readPostOutput($dummyTask); + $this->assertEquals('output2', $output['output']); + $this->assertEquals('errors2', $output['errors']); + $this->assertEquals(102, $output['statusCode']); + + // And to load all $this->assertEquals([ ['output' => 'output0', 'errors' => 'errors0', 'statusCode' => 100], ['output' => 'output1', 'errors' => 'errors1', 'statusCode' => 101], ['output' => 'output2', 'errors' => 'errors2', 'statusCode' => 102] - ], $output); + ], $this->taskStorage->readPostOutput($dummyTask, -1)); } /** diff --git a/test/base/TaskTest.php b/test/base/TaskTest.php index 9215f0a..cebada8 100644 --- a/test/base/TaskTest.php +++ b/test/base/TaskTest.php @@ -35,6 +35,7 @@ */ use FuzeWorks\Async\Constraint; +use FuzeWorks\Async\Handler; use FuzeWorks\Async\Task; use FuzeWorks\Async\TasksException; use Mock\Handlers\EmptyHandler; @@ -111,6 +112,19 @@ class TaskTest extends TestCase $this->assertEquals([$stub], $dummyTask->getConstraints()); } + /** + * @depends testBaseVariables + */ + public function testInitHandler() + { + $mockHandler = $this->createMock(Handler::class); + $mockHandler->expects($this->once())->method('init') + ->with($this->callback(function($subject){return $subject instanceof Task;})); + + // Then create a class + new Task('testInitHandler', $mockHandler); + } + /** * @depends testBaseVariables */ diff --git a/test/config.tasks.php b/test/config.tasks.php index 393ebe6..e3190f1 100644 --- a/test/config.tasks.php +++ b/test/config.tasks.php @@ -83,8 +83,9 @@ return array( 'ShellExecutor' => [ 'parameters' => [ 'workerFile' => Core::getEnv('EXECUTOR_SHELL_WORKER', - dirname(__FILE__) . DS . 'bin' . DS . 'worker'), - 'bootstrapFile' => Core::getEnv('EXECUTOR_SHELL_BOOTSTRAP', 'unknown') + dirname(__FILE__, 2) . DS . 'bin' . DS . 'worker'), + 'bootstrapFile' => Core::getEnv('EXECUTOR_SHELL_BOOTSTRAP', + dirname(__FILE__) . DS . 'bootstrap.php') ] ] ] diff --git a/test/mock/Handlers/ArgumentedHandler.php b/test/mock/Handlers/ArgumentedHandler.php index dd6d458..3b0ae46 100644 --- a/test/mock/Handlers/ArgumentedHandler.php +++ b/test/mock/Handlers/ArgumentedHandler.php @@ -37,6 +37,7 @@ namespace Mock\Handlers; use FuzeWorks\Async\Handler; use FuzeWorks\Async\Task; +use FuzeWorks\Async\TasksException; class ArgumentedHandler implements Handler { @@ -50,6 +51,13 @@ class ArgumentedHandler implements Handler $this->output = $output; } + /** + * @inheritDoc + */ + public function init(Task $task) + { + } + /** * @inheritDoc */ @@ -62,7 +70,7 @@ class ArgumentedHandler implements Handler /** * @inheritDoc */ - public function getOutput() + public function getOutput(): string { return $this->output; } @@ -79,7 +87,7 @@ class ArgumentedHandler implements Handler /** * @inheritDoc */ - public function getPostOutput() + public function getPostOutput(): string { return $this->output; } @@ -95,7 +103,7 @@ class ArgumentedHandler implements Handler /** * @inheritDoc */ - public function setParentInput($input): void + public function setParentInput(string $input): void { } diff --git a/test/mock/Handlers/EmptyHandler.php b/test/mock/Handlers/EmptyHandler.php index 71cf04d..30c5636 100644 --- a/test/mock/Handlers/EmptyHandler.php +++ b/test/mock/Handlers/EmptyHandler.php @@ -41,6 +41,13 @@ use FuzeWorks\Async\Task; class EmptyHandler implements Handler { + /** + * @inheritDoc + */ + public function init(Task $task) + { + } + /** * @inheritDoc */ @@ -52,7 +59,7 @@ class EmptyHandler implements Handler /** * @inheritDoc */ - public function getOutput() + public function getOutput(): string { } @@ -66,7 +73,7 @@ class EmptyHandler implements Handler /** * @inheritDoc */ - public function getPostOutput() + public function getPostOutput(): string { } @@ -81,7 +88,7 @@ class EmptyHandler implements Handler /** * @inheritDoc */ - public function setParentInput($input): void + public function setParentInput(string $input): void { } diff --git a/test/mock/Handlers/TestStartAndReadTasksHandler.php b/test/mock/Handlers/TestStartAndReadTasksHandler.php index eff209c..540d4db 100644 --- a/test/mock/Handlers/TestStartAndReadTasksHandler.php +++ b/test/mock/Handlers/TestStartAndReadTasksHandler.php @@ -41,6 +41,13 @@ use FuzeWorks\Async\Task; class TestStartAndReadTasksHandler implements Handler { + /** + * @inheritDoc + */ + public function init(Task $task) + { + } + /** * @inheritDoc */ @@ -53,7 +60,7 @@ class TestStartAndReadTasksHandler implements Handler /** * @inheritDoc */ - public function getOutput() + public function getOutput(): string { return "Valid Output"; } @@ -69,7 +76,7 @@ class TestStartAndReadTasksHandler implements Handler /** * @inheritDoc */ - public function getPostOutput() + public function getPostOutput(): string { } @@ -85,7 +92,7 @@ class TestStartAndReadTasksHandler implements Handler /** * @inheritDoc */ - public function setParentInput($input): void + public function setParentInput(string $input): void { } diff --git a/test/mock/Handlers/TestStopTaskHandler.php b/test/mock/Handlers/TestStopTaskHandler.php index 8182f77..cebe04a 100644 --- a/test/mock/Handlers/TestStopTaskHandler.php +++ b/test/mock/Handlers/TestStopTaskHandler.php @@ -41,6 +41,13 @@ use FuzeWorks\Async\Task; class TestStopTaskHandler implements Handler { + /** + * @inheritDoc + */ + public function init(Task $task) + { + } + /** * @inheritDoc */ @@ -53,7 +60,7 @@ class TestStopTaskHandler implements Handler /** * @inheritDoc */ - public function getOutput() + public function getOutput(): string { return "Valid Output"; } @@ -68,7 +75,7 @@ class TestStopTaskHandler implements Handler /** * @inheritDoc */ - public function getPostOutput() + public function getPostOutput(): string { } @@ -83,7 +90,7 @@ class TestStopTaskHandler implements Handler /** * @inheritDoc */ - public function setParentInput($input): void + public function setParentInput(string $input): void { } diff --git a/test/system/ParallelSuperVisorTest.php b/test/system/ParallelSuperVisorTest.php index db44e4f..e90613e 100644 --- a/test/system/ParallelSuperVisorTest.php +++ b/test/system/ParallelSuperVisorTest.php @@ -45,6 +45,11 @@ use FuzeWorks\Events; use Mock\Handlers\ArgumentedHandler; use PHPUnit\Framework\TestCase; +/** + * Class ParallelSuperVisorTest + * + * @todo Add test that latest output is added to Task, and not just 'any' output + */ class ParallelSuperVisorTest extends TestCase {