def test_successful_with_partial_recipe(self): """Tests calling QueueManager.handle_job_completion() successfully with a job in a recipe.""" # Queue the recipe recipe_id = Queue.objects.queue_new_recipe(self.recipe_type, self.data, self.event) # Fake out completing Job 1 job_1 = RecipeJob.objects.select_related('job').get(recipe_id=recipe_id, job_name='Job 1').job job_exe_1 = JobExecution.objects.get(job_id=job_1.id) output_file_1 = product_test_utils.create_product(job_exe=job_exe_1, workspace=self.workspace) output_file_2 = product_test_utils.create_product(job_exe=job_exe_1, workspace=self.workspace) results = JobResults() results.add_file_list_parameter('Test Output 1', [output_file_1.id, output_file_2.id]) JobExecution.objects.post_steps_results(job_exe_1.id, results, ResultsManifest()) Job.objects.filter(pk=job_1.id).update(status='RUNNING') JobExecution.objects.filter(pk=job_exe_1.id).update(status='RUNNING') # Call method to test Queue.objects.handle_job_completion(job_exe_1.id, now()) # Make sure processor was called self.assertTrue(self.mock_processor.process_completed.called) # Make sure Job 2 in the recipe is successfully queued recipe_job_2 = RecipeJob.objects.select_related('job', 'recipe').get(recipe_id=recipe_id, job_name='Job 2') self.assertEqual(recipe_job_2.job.status, 'QUEUED') self.assertIsNone(recipe_job_2.recipe.completed)
def test_successful_with_full_recipe(self): """Tests calling QueueManager.handle_job_completion() successfully with all jobs in a recipe.""" # Queue the recipe handler = Queue.objects.queue_new_recipe(self.recipe_type, self.data, self.event) # Fake out completing Job 1 job_1 = RecipeJob.objects.select_related('job').get( recipe_id=handler.recipe.id, job_name='Job 1').job job_exe_1 = job_test_utils.create_job_exe(job=job_1, status='RUNNING') output_file_1 = product_test_utils.create_product( job_exe=job_exe_1, workspace=self.workspace) output_file_2 = product_test_utils.create_product( job_exe=job_exe_1, workspace=self.workspace) results = JobResults() results.add_file_list_parameter('Test Output 1', [output_file_1.id, output_file_2.id]) job_exe_output_1 = JobExecutionOutput() job_exe_output_1.job_exe_id = job_exe_1.id job_exe_output_1.job_id = job_exe_1.job_id job_exe_output_1.job_type_id = job_exe_1.job_type_id job_exe_output_1.exe_num = job_exe_1.exe_num job_exe_output_1.output = results.get_dict() job_exe_output_1.save() Job.objects.filter(pk=job_1.id).update(status='RUNNING') Queue.objects.handle_job_completion(job_1.id, job_1.num_exes, now()) # Fake out completing Job 2 job_2 = RecipeJob.objects.select_related('job').get( recipe_id=handler.recipe.id, job_name='Job 2').job job_exe_2 = job_test_utils.create_job_exe(job=job_2, status='RUNNING') output_file_1 = product_test_utils.create_product( job_exe=job_exe_2, workspace=self.workspace) output_file_2 = product_test_utils.create_product( job_exe=job_exe_2, workspace=self.workspace) results = JobResults() results.add_file_list_parameter('Test Output 2', [output_file_1.id, output_file_2.id]) job_exe_output_2 = JobExecutionOutput() job_exe_output_2.job_exe_id = job_exe_2.id job_exe_output_2.job_id = job_exe_2.job_id job_exe_output_2.job_type_id = job_exe_2.job_type_id job_exe_output_2.exe_num = job_exe_2.exe_num job_exe_output_2.output = results.get_dict() job_exe_output_2.save() Job.objects.filter(pk=job_2.id).update(status='RUNNING') # Call method to test Queue.objects.handle_job_completion(job_2.id, job_2.num_exes, now()) # Make sure final recipe attributes are updated recipe = Recipe.objects.get(pk=handler.recipe.id) self.assertIsNotNone(recipe.completed)
def test_successful_file_list(self): '''Tests calling JobResults.add_output_to_data() successfully with a file list parameter''' output_name = u'foo' file_ids = [1, 2, 3, 4] input_name = u'bar' results = JobResults() results.add_file_list_parameter(output_name, file_ids) job_data = MagicMock() results.add_output_to_data(output_name, job_data, input_name) job_data.add_file_list_input.assert_called_with(input_name, file_ids)
def test_successful_file_list(self): """Tests calling JobResults.add_output_to_data() successfully with a file list parameter""" output_name = 'foo' file_ids = [1, 2, 3, 4] input_name = 'bar' results = JobResults() results.add_file_list_parameter(output_name, file_ids) job_data = MagicMock() results.add_output_to_data(output_name, job_data, input_name) job_data.add_file_list_input.assert_called_with(input_name, file_ids)
def test_successful_file(self): '''Tests calling JobResults.add_output_to_data() successfully with a file parameter''' output_name = u'foo' file_id = 1337 input_name = u'bar' results = JobResults() results.add_file_parameter(output_name, file_id) job_data = MagicMock() results.add_output_to_data(output_name, job_data, input_name) job_data.add_file_input.assert_called_with(input_name, file_id)
def test_successful_with_full_recipe(self): """Tests calling QueueManager.handle_job_completion() successfully with all jobs in a recipe.""" # Queue the recipe recipe_id = Queue.objects.queue_new_recipe(self.recipe_type, self.data, self.event) # Fake out completing Job 1 job_1 = RecipeJob.objects.select_related("job").get(recipe_id=recipe_id, job_name="Job 1").job job_exe_1 = JobExecution.objects.get(job_id=job_1.id) output_file_1 = product_test_utils.create_product(job_exe=job_exe_1, workspace=self.workspace) output_file_2 = product_test_utils.create_product(job_exe=job_exe_1, workspace=self.workspace) results = JobResults() results.add_file_list_parameter("Test Output 1", [output_file_1.id, output_file_2.id]) JobExecution.objects.post_steps_results(job_exe_1.id, results, ResultsManifest()) Job.objects.filter(pk=job_1.id).update(status="RUNNING") JobExecution.objects.filter(pk=job_exe_1.id).update(status="RUNNING") Queue.objects.handle_job_completion(job_exe_1.id, now()) # Fake out completing Job 2 job_2 = RecipeJob.objects.select_related("job").get(recipe_id=recipe_id, job_name="Job 2").job job_exe_2 = JobExecution.objects.get(job_id=job_2.id) output_file_1 = product_test_utils.create_product(job_exe=job_exe_2, workspace=self.workspace) output_file_2 = product_test_utils.create_product(job_exe=job_exe_2, workspace=self.workspace) results = JobResults() results.add_file_list_parameter("Test Output 2", [output_file_1.id, output_file_2.id]) JobExecution.objects.post_steps_results(job_exe_2.id, results, ResultsManifest()) Job.objects.filter(pk=job_2.id).update(status="RUNNING") JobExecution.objects.filter(pk=job_exe_2.id).update(status="RUNNING") # Call method to test Queue.objects.handle_job_completion(job_exe_2.id, now()) # Make sure processor was called self.assertEqual(self.mock_processor.process_completed.call_count, 2) # Make sure final recipe attributes are updated recipe = Recipe.objects.get(pk=recipe_id) self.assertIsNotNone(recipe.completed)
def test_execute(self): """Tests calling CompletedJobs.execute() successfully""" from recipe.test import utils as recipe_test_utils recipe_1 = recipe_test_utils.create_recipe() job_1 = job_test_utils.create_job(num_exes=1, status='QUEUED') job_test_utils.create_job_exe(job=job_1) job_2 = job_test_utils.create_job(num_exes=1, status='RUNNING', recipe=recipe_1) job_test_utils.create_job_exe(job=job_2, output=JobResults()) job_3 = job_test_utils.create_job(num_exes=0, status='PENDING') job_ids = [job_1.id, job_2.id, job_3.id] recipe_test_utils.create_recipe_job(recipe=recipe_1, job=job_2) when_ended = now() # Add jobs to message message = CompletedJobs() message.ended = when_ended if message.can_fit_more(): message.add_completed_job(CompletedJob(job_1.id, job_1.num_exes)) if message.can_fit_more(): message.add_completed_job(CompletedJob(job_2.id, job_2.num_exes)) if message.can_fit_more(): message.add_completed_job(CompletedJob(job_3.id, job_3.num_exes)) # Execute message result = message.execute() self.assertTrue(result) from recipe.diff.forced_nodes import ForcedNodes from recipe.diff.json.forced_nodes_v6 import convert_forced_nodes_to_v6 forced_nodes = ForcedNodes() forced_nodes.set_all_nodes() forced_nodes_dict = convert_forced_nodes_to_v6(forced_nodes).get_dict() jobs = Job.objects.filter(id__in=job_ids).order_by('id') self.assertEqual(len(message.new_messages), 3) update_recipe_metrics_msg = None update_recipe_msg = None publish_job_msg = None for msg in message.new_messages: if msg.type == 'update_recipe': update_recipe_msg = msg elif msg.type == 'publish_job': publish_job_msg = msg elif msg.type == 'update_recipe_metrics': update_recipe_metrics_msg = msg self.assertIsNotNone(update_recipe_msg) self.assertIsNotNone(publish_job_msg) self.assertIsNotNone(update_recipe_metrics_msg) self.assertEqual(publish_job_msg.job_id, job_2.id) # Job 1 should be completed self.assertEqual(jobs[0].status, 'COMPLETED') self.assertEqual(jobs[0].num_exes, 1) self.assertEqual(jobs[0].ended, when_ended) # Job 2 should be completed and has output, so should be in update_recipe message self.assertEqual(jobs[1].status, 'COMPLETED') self.assertEqual(jobs[1].num_exes, 1) self.assertEqual(jobs[1].ended, when_ended) self.assertEqual(update_recipe_msg.root_recipe_id, recipe_1.id) self.assertDictEqual( convert_forced_nodes_to_v6( update_recipe_msg.forced_nodes).get_dict(), forced_nodes_dict) # Job 3 should ignore update self.assertEqual(jobs[2].status, 'PENDING') self.assertEqual(jobs[2].num_exes, 0) # Test executing message again new_ended = when_ended + datetime.timedelta(minutes=5) message_json_dict = message.to_json() message = CompletedJobs.from_json(message_json_dict) message.ended = new_ended result = message.execute() self.assertTrue(result) # Should have the same messages as before jobs = Job.objects.filter(id__in=job_ids).order_by('id') self.assertEqual(len(message.new_messages), 3) update_recipe_metrics_msg = None update_recipe_msg = None publish_job_msg = None for msg in message.new_messages: if msg.type == 'update_recipe': update_recipe_msg = msg elif msg.type == 'publish_job': publish_job_msg = msg elif msg.type == 'update_recipe_metrics': update_recipe_metrics_msg = msg self.assertIsNotNone(update_recipe_msg) self.assertIsNotNone(publish_job_msg) self.assertIsNotNone(update_recipe_metrics_msg) self.assertEqual(publish_job_msg.job_id, job_2.id) # Job 1 should be completed self.assertEqual(jobs[0].status, 'COMPLETED') self.assertEqual(jobs[0].num_exes, 1) self.assertEqual(jobs[0].ended, when_ended) # Job 2 should be completed and has output, so should be in update_recipe message self.assertEqual(jobs[1].status, 'COMPLETED') self.assertEqual(jobs[1].num_exes, 1) self.assertEqual(jobs[1].ended, when_ended) self.assertEqual(update_recipe_msg.root_recipe_id, recipe_1.id) self.assertDictEqual( convert_forced_nodes_to_v6( update_recipe_msg.forced_nodes).get_dict(), forced_nodes_dict) # Job 3 should ignore update self.assertEqual(jobs[2].status, 'PENDING') self.assertEqual(jobs[2].num_exes, 0)
def store_output_data_files(self, data_files, job_exe): """Stores the given data output files :param data_files: Dict with each file parameter name mapping to a list of ProductFileMetadata classes :type data_files: {string: [`ProductFileMetadata`]} :param job_exe: The job execution model (with related job and job_type fields) that is storing the output data files :type job_exe: :class:`job.models.JobExecution` :returns: The job results :rtype: :class:`job.configuration.results.job_results.JobResults` """ # Organize the data files workspace_files = { } # Workspace ID -> [(absolute local file path, media type)] params_by_file_path = { } # Absolute local file path -> output parameter name output_workspaces = JobData.create_output_workspace_dict( data_files.keys(), self, job_exe) for name in data_files: workspace_id = output_workspaces[name] if workspace_id in workspace_files: workspace_file_list = workspace_files[workspace_id] else: workspace_file_list = [] workspace_files[workspace_id] = workspace_file_list data_file_entry = data_files[name] if isinstance(data_file_entry, list): for file_entry in data_file_entry: file_path = os.path.normpath(file_entry.local_path) if not os.path.isfile(file_path): raise Exception('%s is not a valid file' % file_path) params_by_file_path[file_path] = name workspace_file_list.append(file_entry) else: file_path = os.path.normpath(data_file_entry.local_path) if not os.path.isfile(file_path): raise Exception('%s is not a valid file' % file_path) params_by_file_path[file_path] = name data_file_entry.local_path = file_path workspace_file_list.append(data_file_entry) data_file_store = DATA_FILE_STORE['DATA_FILE_STORE'] if not data_file_store: raise Exception('No data file store found') stored_files = data_file_store.store_files(workspace_files, self.get_input_file_ids(), job_exe) # Organize results param_file_ids = {} # Output parameter name -> file ID or [file IDs] for file_path in stored_files: file_id = stored_files[file_path] name = params_by_file_path[file_path] if isinstance(data_files[name], list): if name in param_file_ids: file_id_list = param_file_ids[name] else: file_id_list = [] param_file_ids[name] = file_id_list file_id_list.append(file_id) else: param_file_ids[name] = file_id # Create job results results = JobResults() for name in param_file_ids: param_entry = param_file_ids[name] if isinstance(param_entry, list): results.add_file_list_parameter(name, param_entry) else: results.add_file_parameter(name, param_entry) return results
def store_output_data_files(self, data_files, job_exe): """Stores the given data output files :param data_files: Dict with each file parameter name mapping to a tuple of absolute local file path and media type (media type is optionally None) for a single file parameter and a list of tuples for a multiple file parameter :type data_files: {string: tuple(string, string)} or [tuple(string, string)] :param job_exe: The job execution model (with related job and job_type fields) that is storing the output data files :type job_exe: :class:`job.models.JobExecution` :returns: The job results :rtype: :class:`job.configuration.results.job_results.JobResults` """ # Organize the data files workspace_files = { } # Workspace ID -> [(absolute local file path, media type)] params_by_file_path = { } # Absolute local file path -> output parameter name for name in data_files: file_output = self.data_outputs_by_name[name] workspace_id = file_output['workspace_id'] if workspace_id in workspace_files: workspace_file_list = workspace_files[workspace_id] else: workspace_file_list = [] workspace_files[workspace_id] = workspace_file_list data_file_entry = data_files[name] if isinstance(data_file_entry, list): for file_tuple in data_file_entry: file_path = os.path.normpath(file_tuple[0]) if not os.path.isfile(file_path): raise Exception('%s is not a valid file' % file_path) params_by_file_path[file_path] = name # Adjust file path to be relative to upload_dir if len(file_tuple) == 2: new_tuple = (file_path, file_tuple[1], name) else: new_tuple = (file_path, file_tuple[1], name, file_tuple[2]) workspace_file_list.append(new_tuple) else: file_path = os.path.normpath(data_file_entry[0]) if not os.path.isfile(file_path): raise Exception('%s is not a valid file' % file_path) params_by_file_path[file_path] = name # Adjust file path to be relative to upload_dir if len(data_file_entry) == 2: new_tuple = (file_path, data_file_entry[1], name) else: new_tuple = (file_path, data_file_entry[1], name, data_file_entry[2]) workspace_file_list.append(new_tuple) data_file_store = DATA_FILE_STORE['DATA_FILE_STORE'] if not data_file_store: raise Exception('No data file store found') stored_files = data_file_store.store_files(workspace_files, self.get_input_file_ids(), job_exe) # Organize results param_file_ids = {} # Output parameter name -> file ID or [file IDs] for file_path in stored_files: file_id = stored_files[file_path] name = params_by_file_path[file_path] if isinstance(data_files[name], list): if name in param_file_ids: file_id_list = param_file_ids[name] else: file_id_list = [] param_file_ids[name] = file_id_list file_id_list.append(file_id) else: param_file_ids[name] = file_id # Create job results results = JobResults() for name in param_file_ids: param_entry = param_file_ids[name] if isinstance(param_entry, list): results.add_file_list_parameter(name, param_entry) else: results.add_file_parameter(name, param_entry) return results
def create_job_exe(job_type=None, job=None, exe_num=None, node=None, timeout=None, input_file_size=10.0, queued=None, started=None, status='RUNNING', error=None, ended=None, output=None, task_results=None): """Creates a job_exe model for unit testing, may also create job_exe_end and job_exe_output models depending on status :returns: The job_exe model :rtype: :class:`job.execution.job_exe.RunningJobExecution` """ when = timezone.now() if not job: job = create_job(job_type=job_type, status=status, input_file_size=input_file_size) job_type = job.job_type job_exe = JobExecution() job_exe.job = job job_exe.job_type = job_type if not exe_num: exe_num = job.num_exes job_exe.exe_num = exe_num job_exe.set_cluster_id('1234', job.id, job_exe.exe_num) if not node: node = node_utils.create_node() job_exe.node = node if not timeout: timeout = job.timeout job_exe.timeout = timeout job_exe.input_file_size = input_file_size job_exe.resources = job.get_resources().get_json().get_dict() job_exe.configuration = ExecutionConfiguration().get_dict() if not queued: queued = when job_exe.queued = queued if not started: started = when + datetime.timedelta(seconds=1) job_exe.started = started job_exe.save() if status in ['COMPLETED', 'FAILED', 'CANCELED']: job_exe_end = JobExecutionEnd() job_exe_end.job_exe_id = job_exe.id job_exe_end.job = job_exe.job job_exe_end.job_type = job_exe.job_type job_exe_end.exe_num = job_exe.exe_num if not task_results: task_results = TaskResults() job_exe_end.task_results = task_results.get_dict() job_exe_end.status = status if status == 'FAILED' and not error: error = error_test_utils.create_error() job_exe_end.error = error job_exe_end.node = node job_exe_end.queued = queued job_exe_end.started = started job_exe_end.seed_started = task_results.get_task_started('main') job_exe_end.seed_ended = task_results.get_task_ended('main') if not ended: ended = started + datetime.timedelta(seconds=1) job_exe_end.ended = ended job_exe_end.save() if status == 'COMPLETED' or output: job_exe_output = JobExecutionOutput() job_exe_output.job_exe_id = job_exe.id job_exe_output.job = job_exe.job job_exe_output.job_type = job_exe.job_type job_exe_output.exe_num = job_exe.exe_num if not output: output = JobResults() job_exe_output.output = output.get_dict() job_exe_output.save() return job_exe
def test_successful_supersede(self): """Tests calling QueueManager.queue_new_recipe() successfully when superseding a recipe.""" # Queue initial recipe and complete its first job node = node_test_utils.create_node() recipe_id = Queue.objects.queue_new_recipe(self.recipe_type, self.data, self.event) recipe = Recipe.objects.get(id=recipe_id) recipe_job_1 = RecipeJob.objects.select_related('job__job_exe').get(recipe_id=recipe_id, job_name='Job 1') job_exe_1 = JobExecution.objects.get(job_id=recipe_job_1.job_id) queued_job_exe = QueuedJobExecution(Queue.objects.get(job_exe_id=job_exe_1.id)) queued_job_exe.accepted(node, JobResources(cpus=10, mem=1000, disk_in=1000, disk_out=1000, disk_total=2000)) Queue.objects.schedule_job_executions('123', [queued_job_exe], {}) results = JobResults() results.add_file_list_parameter('Test Output 1', [product_test_utils.create_product().file_id]) JobExecution.objects.filter(id=job_exe_1.id).update(results=results.get_dict()) Queue.objects.handle_job_completion(job_exe_1.id, now()) # Create a new recipe type that has a new version of job 2 (job 1 is identical) new_job_type_2 = job_test_utils.create_job_type(name=self.job_type_2.name, version='New Version', interface=self.job_type_2.interface) new_definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'New Job 1', 'job_type': { 'name': self.job_type_1.name, 'version': self.job_type_1.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': 'Test Input 1', }] }, { 'name': 'New Job 2', 'job_type': { 'name': new_job_type_2.name, 'version': new_job_type_2.version, }, 'dependencies': [{ 'name': 'New Job 1', 'connections': [{ 'output': 'Test Output 1', 'input': 'Test Input 2', }] }] }] } new_recipe_type = recipe_test_utils.create_recipe_type(name=self.recipe_type.name, definition=new_definition) event = trigger_test_utils.create_trigger_event() recipe_job_1 = RecipeJob.objects.select_related('job').get(recipe_id=recipe_id, job_name='Job 1') recipe_job_2 = RecipeJob.objects.select_related('job').get(recipe_id=recipe_id, job_name='Job 2') superseded_jobs = {'Job 1': recipe_job_1.job, 'Job 2': recipe_job_2.job} graph_a = self.recipe_type.get_recipe_definition().get_graph() graph_b = new_recipe_type.get_recipe_definition().get_graph() delta = RecipeGraphDelta(graph_a, graph_b) # Queue new recipe that supersedes the old recipe new_recipe_id = Queue.objects.queue_new_recipe(new_recipe_type, None, event, recipe, delta, superseded_jobs) # Ensure old recipe is superseded recipe = Recipe.objects.get(id=recipe_id) self.assertTrue(recipe.is_superseded) # Ensure new recipe supersedes old recipe new_recipe = Recipe.objects.get(id=new_recipe_id) self.assertEqual(new_recipe.superseded_recipe_id, recipe_id) # Ensure that job 1 is already completed (it was copied from original recipe) and that job 2 is queued new_recipe_job_1 = RecipeJob.objects.select_related('job').get(recipe_id=new_recipe_id, job_name='New Job 1') new_recipe_job_2 = RecipeJob.objects.select_related('job').get(recipe_id=new_recipe_id, job_name='New Job 2') self.assertEqual(new_recipe_job_1.job.status, 'COMPLETED') self.assertFalse(new_recipe_job_1.is_original) self.assertEqual(new_recipe_job_2.job.status, 'QUEUED') self.assertTrue(new_recipe_job_2.is_original) # Complete both the old and new job 2 and check that only the new recipe completes job_exe_2 = JobExecution.objects.get(job_id=recipe_job_2.job_id) queued_job_exe_2 = QueuedJobExecution(Queue.objects.get(job_exe_id=job_exe_2.id)) queued_job_exe_2.accepted(node, JobResources(cpus=10, mem=1000, disk_in=1000, disk_out=1000, disk_total=2000)) Queue.objects.schedule_job_executions('123', [queued_job_exe_2], {}) Queue.objects.handle_job_completion(job_exe_2.id, now()) new_job_exe_2 = JobExecution.objects.get(job_id=new_recipe_job_2.job_id) new_queued_job_exe_2 = QueuedJobExecution(Queue.objects.get(job_exe_id=new_job_exe_2.id)) new_queued_job_exe_2.accepted(node, JobResources(cpus=10, mem=1000, disk_in=1000, disk_out=1000, disk_total=2000)) Queue.objects.schedule_job_executions('123', [new_queued_job_exe_2], {}) Queue.objects.handle_job_completion(new_job_exe_2.id, now()) recipe = Recipe.objects.get(id=recipe.id) new_recipe = Recipe.objects.get(id=new_recipe.id) self.assertIsNone(recipe.completed) self.assertIsNotNone(new_recipe.completed)
def store_output_data_files(self, data_files, job_exe): """Stores the given data output files :param data_files: Dict with each file parameter name mapping to a tuple of absolute local file path and media type (media type is optionally None) for a single file parameter and a list of tuples for a multiple file parameter :type data_files: dict of str -> tuple(str, str) or list of tuple(str, str) :param job_exe: The job execution model (with related job and job_type fields) that is storing the output data files :type job_exe: :class:`job.models.JobExecution` :returns: The job results :rtype: :class:`job.configuration.results.job_results.JobResults` """ # Organize the data files workspace_files = {} # Workspace ID -> list of (absolute local file path, media type) params_by_file_path = {} # Absolute local file path -> output parameter name for name in data_files: file_output = self.data_outputs_by_name[name] workspace_id = file_output['workspace_id'] if workspace_id in workspace_files: workspace_file_list = workspace_files[workspace_id] else: workspace_file_list = [] workspace_files[workspace_id] = workspace_file_list data_file_entry = data_files[name] if isinstance(data_file_entry, list): for file_tuple in data_file_entry: file_path = os.path.normpath(file_tuple[0]) if not os.path.isfile(file_path): raise Exception('%s is not a valid file' % file_path) params_by_file_path[file_path] = name # Adjust file path to be relative to upload_dir if len(file_tuple) == 2: new_tuple = (file_path, file_tuple[1]) else: new_tuple = (file_path, file_tuple[1], file_tuple[2]) workspace_file_list.append(new_tuple) else: file_path = os.path.normpath(data_file_entry[0]) if not os.path.isfile(file_path): raise Exception('%s is not a valid file' % file_path) params_by_file_path[file_path] = name # Adjust file path to be relative to upload_dir if len(data_file_entry) == 2: new_tuple = (file_path, data_file_entry[1]) else: new_tuple = (file_path, data_file_entry[1], data_file_entry[2]) workspace_file_list.append(new_tuple) data_file_store = DATA_FILE_STORE['DATA_FILE_STORE'] if not data_file_store: raise Exception('No data file store found') stored_files = data_file_store.store_files(workspace_files, self.get_input_file_ids(), job_exe) # Organize results param_file_ids = {} # Output parameter name -> file ID or list of file IDs for file_path in stored_files: file_id = stored_files[file_path] name = params_by_file_path[file_path] if isinstance(data_files[name], list): if name in param_file_ids: file_id_list = param_file_ids[name] else: file_id_list = [] param_file_ids[name] = file_id_list file_id_list.append(file_id) else: param_file_ids[name] = file_id # Create job results results = JobResults() for name in param_file_ids: param_entry = param_file_ids[name] if isinstance(param_entry, list): results.add_file_list_parameter(name, param_entry) else: results.add_file_parameter(name, param_entry) return results
def test_successful_supersede_mixed(self): """Tests calling QueueManager.queue_new_recipe() successfully when superseding a recipe where the results of a Seed job get passed to the input of a legacy job """ workspace = storage_test_utils.create_workspace() source_file = source_test_utils.create_source(workspace=workspace) event = trigger_test_utils.create_trigger_event() interface_1 = { 'seedVersion': '1.0.0', 'job': { 'name': 'job-type-a', 'jobVersion': '1.0.0', 'packageVersion': '1.0.0', 'title': 'Job Type 1', 'description': 'This is a description', 'maintainer': { 'name': 'John Doe', 'email': '*****@*****.**' }, 'timeout': 10, 'interface': { 'command': '', 'inputs': { 'files': [{ 'name': 'test-input-a' }] }, 'outputs': { 'files': [{ 'name': 'test-output-a', 'pattern': '*.png' }] } } } } job_type_1 = job_test_utils.create_seed_job_type(manifest=interface_1) interface_2 = { 'version': '1.0', 'command': 'test_command', 'command_arguments': 'test_arg', 'input_data': [{ 'name': 'Test Input 2', 'type': 'file', 'media_types': ['image/png', 'image/tiff'], }], 'output_data': [{ 'name': 'Test Output 2', 'type': 'file', }] } job_type_2 = job_test_utils.create_job_type(interface=interface_2) definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'Job 1', 'job_type': { 'name': job_type_1.name, 'version': job_type_1.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': 'test-input-a', }] }, { 'name': 'Job 2', 'job_type': { 'name': job_type_2.name, 'version': job_type_2.version, }, 'dependencies': [{ 'name': 'Job 1', 'connections': [{ 'output': 'test-output-a', 'input': 'Test Input 2', }] }] }] } recipe_definition = RecipeDefinition(definition) recipe_definition.validate_job_interfaces() recipe_type = recipe_test_utils.create_recipe_type( definition=definition) data = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'file_id': source_file.id, }], 'workspace_id': workspace.id, } data = LegacyRecipeData(data) # Queue initial recipe and complete its first job handler = Queue.objects.queue_new_recipe(recipe_type, data, event) recipe = Recipe.objects.get(id=handler.recipe.id) recipe_job_1 = RecipeNode.objects.select_related('job') recipe_job_1 = recipe_job_1.get(recipe_id=handler.recipe.id, node_name='Job 1') Job.objects.update_jobs_to_running([recipe_job_1.job], now()) results = JobResults() results.add_file_parameter('test-output-a', product_test_utils.create_product().id) job_test_utils.create_job_exe(job=recipe_job_1.job, status='COMPLETED', output=results) Job.objects.update_jobs_to_completed([recipe_job_1.job], now()) Job.objects.process_job_output([recipe_job_1.job_id], now()) # Create a new recipe type that has a new version of job 2 (job 1 is identical) new_job_type_2 = job_test_utils.create_job_type( name=job_type_2.name, version='New Version', interface=job_type_2.manifest) new_definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'New Job 1', 'job_type': { 'name': job_type_1.name, 'version': job_type_1.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': 'test-input-a', }] }, { 'name': 'New Job 2', 'job_type': { 'name': new_job_type_2.name, 'version': new_job_type_2.version, }, 'dependencies': [{ 'name': 'New Job 1', 'connections': [{ 'output': 'test-output-a', 'input': 'Test Input 2', }] }] }] } new_recipe_type = recipe_test_utils.create_recipe_type( name=recipe_type.name, definition=new_definition) event = trigger_test_utils.create_trigger_event() recipe_job_1 = RecipeNode.objects.select_related('job').get( recipe_id=handler.recipe.id, node_name='Job 1') recipe_job_2 = RecipeNode.objects.select_related('job').get( recipe_id=handler.recipe.id, node_name='Job 2') superseded_jobs = { 'Job 1': recipe_job_1.job, 'Job 2': recipe_job_2.job } graph_a = recipe_type.get_recipe_definition().get_graph() graph_b = new_recipe_type.get_recipe_definition().get_graph() delta = RecipeGraphDelta(graph_a, graph_b) # Queue new recipe that supersedes the old recipe new_handler = Queue.objects.queue_new_recipe( new_recipe_type, None, event, superseded_recipe=recipe, delta=delta, superseded_jobs=superseded_jobs) # Ensure old recipe is superseded recipe = Recipe.objects.get(id=handler.recipe.id) self.assertTrue(recipe.is_superseded) # Ensure new recipe supersedes old recipe new_recipe = Recipe.objects.get(id=new_handler.recipe.id) self.assertEqual(new_recipe.superseded_recipe_id, handler.recipe.id) # Ensure that job 1 is already completed (it was copied from original recipe) and that job 2 is queued new_recipe_job_1 = RecipeNode.objects.select_related('job').get( recipe_id=new_handler.recipe.id, node_name='New Job 1') new_recipe_job_2 = RecipeNode.objects.select_related('job').get( recipe_id=new_handler.recipe.id, node_name='New Job 2') self.assertEqual(new_recipe_job_1.job.status, 'COMPLETED') self.assertFalse(new_recipe_job_1.is_original) self.assertEqual(new_recipe_job_2.job.status, 'QUEUED') self.assertTrue(new_recipe_job_2.is_original)
def test_successful_supersede(self): """Tests calling QueueManager.queue_new_recipe() successfully when superseding a recipe.""" # Queue initial recipe and complete its first job handler = Queue.objects.queue_new_recipe(self.recipe_type, self.data, self.event) recipe = Recipe.objects.get(id=handler.recipe.id) recipe_job_1 = RecipeNode.objects.select_related('job') recipe_job_1 = recipe_job_1.get(recipe_id=handler.recipe.id, node_name='Job 1') Job.objects.update_jobs_to_running([recipe_job_1.job], now()) results = JobResults() results.add_file_list_parameter( 'Test Output 1', [product_test_utils.create_product().id]) job_test_utils.create_job_exe(job=recipe_job_1.job, status='COMPLETED', output=results) Job.objects.update_jobs_to_completed([recipe_job_1.job], now()) Job.objects.process_job_output([recipe_job_1.job_id], now()) # Create a new recipe type that has a new version of job 2 (job 1 is identical) new_job_type_2 = job_test_utils.create_job_type( name=self.job_type_2.name, version='New Version', interface=self.job_type_2.manifest) new_definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'New Job 1', 'job_type': { 'name': self.job_type_1.name, 'version': self.job_type_1.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': 'Test Input 1', }] }, { 'name': 'New Job 2', 'job_type': { 'name': new_job_type_2.name, 'version': new_job_type_2.version, }, 'dependencies': [{ 'name': 'New Job 1', 'connections': [{ 'output': 'Test Output 1', 'input': 'Test Input 2', }] }] }] } new_recipe_type = recipe_test_utils.create_recipe_type( name=self.recipe_type.name, definition=new_definition) event = trigger_test_utils.create_trigger_event() recipe_job_1 = RecipeNode.objects.select_related('job').get( recipe_id=handler.recipe.id, node_name='Job 1') recipe_job_2 = RecipeNode.objects.select_related('job').get( recipe_id=handler.recipe.id, node_name='Job 2') superseded_jobs = { 'Job 1': recipe_job_1.job, 'Job 2': recipe_job_2.job } graph_a = self.recipe_type.get_recipe_definition().get_graph() graph_b = new_recipe_type.get_recipe_definition().get_graph() delta = RecipeGraphDelta(graph_a, graph_b) # Queue new recipe that supersedes the old recipe new_handler = Queue.objects.queue_new_recipe( new_recipe_type, None, event, superseded_recipe=recipe, delta=delta, superseded_jobs=superseded_jobs) # Ensure old recipe is superseded recipe = Recipe.objects.get(id=handler.recipe.id) self.assertTrue(recipe.is_superseded) # Ensure new recipe supersedes old recipe new_recipe = Recipe.objects.get(id=new_handler.recipe.id) self.assertEqual(new_recipe.superseded_recipe_id, handler.recipe.id) # Ensure that job 1 is already completed (it was copied from original recipe) and that job 2 is queued new_recipe_job_1 = RecipeNode.objects.select_related('job').get( recipe_id=new_handler.recipe.id, node_name='New Job 1') new_recipe_job_2 = RecipeNode.objects.select_related('job').get( recipe_id=new_handler.recipe.id, node_name='New Job 2') self.assertEqual(new_recipe_job_1.job.status, 'COMPLETED') self.assertFalse(new_recipe_job_1.is_original) self.assertEqual(new_recipe_job_2.job.status, 'QUEUED') self.assertTrue(new_recipe_job_2.is_original)
def test_execute(self): """Tests calling UpdateRecipes.execute() successfully""" # Create recipes for testing the setting of jobs to BLOCKED/PENDING self.job_1_failed = job_test_utils.create_job(status='FAILED') self.job_1_pending = job_test_utils.create_job(status='PENDING') definition_1 = { 'version': '1.0', 'input_data': [], 'jobs': [{ 'name': 'job_failed', 'job_type': { 'name': self.job_1_failed.job_type.name, 'version': self.job_1_failed.job_type.version, }, }, { 'name': 'job_pending', 'job_type': { 'name': self.job_1_pending.job_type.name, 'version': self.job_1_pending.job_type.version, }, 'dependencies': [{ 'name': 'job_failed', }], }], } self.recipe_type_1 = recipe_test_utils.create_recipe_type(definition=definition_1) self.recipe_1 = recipe_test_utils.create_recipe(recipe_type=self.recipe_type_1) recipe_test_utils.create_recipe_job(recipe=self.recipe_1, job_name='job_failed', job=self.job_1_failed) recipe_test_utils.create_recipe_job(recipe=self.recipe_1, job_name='job_pending', job=self.job_1_pending) self.job_2_running = job_test_utils.create_job(status='RUNNING') self.job_2_blocked = job_test_utils.create_job(status='BLOCKED') definition_2 = { 'version': '1.0', 'input_data': [], 'jobs': [{ 'name': 'job_running', 'job_type': { 'name': self.job_2_running.job_type.name, 'version': self.job_2_running.job_type.version, }, }, { 'name': 'job_blocked', 'job_type': { 'name': self.job_2_blocked.job_type.name, 'version': self.job_2_blocked.job_type.version, }, 'dependencies': [{ 'name': 'job_running', }], }], } self.recipe_type_2 = recipe_test_utils.create_recipe_type(definition=definition_2) self.recipe_2 = recipe_test_utils.create_recipe(recipe_type=self.recipe_type_2) recipe_test_utils.create_recipe_job(recipe=self.recipe_2, job_name='job_running', job=self.job_2_running) recipe_test_utils.create_recipe_job(recipe=self.recipe_2, job_name='job_blocked', job=self.job_2_blocked) # Create recipe for testing the setting of input for a starting job in a recipe (no parents) input_name_1 = 'Test Input 1' output_name_1 = 'Test Output 1' interface_1 = { 'version': '1.0', 'command': 'my_cmd', 'command_arguments': 'args', 'input_data': [{ 'name': input_name_1, 'type': 'file', 'media_types': ['text/plain'], }], 'output_data': [{ 'name': output_name_1, 'type': 'files', 'media_type': 'image/png', }], } job_type_3 = job_test_utils.create_job_type(interface=interface_1) job_3 = job_test_utils.create_job(job_type=job_type_3, status='PENDING', num_exes=0) input_name_2 = 'Test Input 2' output_name_2 = 'Test Output 2' interface_2 = { 'version': '1.0', 'command': 'my_cmd', 'command_arguments': 'args', 'input_data': [{ 'name': input_name_2, 'type': 'files', 'media_types': ['image/png', 'image/tiff'], }], 'output_data': [{ 'name': output_name_2, 'type': 'file', }], } job_type_4 = job_test_utils.create_job_type(interface=interface_2) job_4 = job_test_utils.create_job(job_type=job_type_4, status='PENDING', num_exes=0) workspace = storage_test_utils.create_workspace() file_1 = storage_test_utils.create_file(workspace=workspace, media_type='text/plain') definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'Job 1', 'job_type': { 'name': job_type_3.name, 'version': job_type_3.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': input_name_1, }] }, { 'name': 'Job 2', 'job_type': { 'name': job_type_4.name, 'version': job_type_4.version, }, 'dependencies': [{ 'name': 'Job 1', 'connections': [{ 'output': output_name_1, 'input': input_name_2, }], }], }], } data = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'file_id': file_1.id, }], 'workspace_id': workspace.id, } self.recipe_type_3 = recipe_test_utils.create_recipe_type(definition=definition) self.recipe_3 = recipe_test_utils.create_recipe(recipe_type=self.recipe_type_3, input=data) recipe_test_utils.create_recipe_job(recipe=self.recipe_3, job_name='Job 1', job=job_3) recipe_test_utils.create_recipe_job(recipe=self.recipe_3, job_name='Job 2', job=job_4) # Create recipe for testing the setting of input for a child job job_5 = job_test_utils.create_job(job_type=job_type_3, status='COMPLETED') file_2 = storage_test_utils.create_file(workspace=workspace, media_type='text/plain') job_5_output_dict = { 'version': '1.0', 'output_data': [{ 'name': output_name_1, 'file_ids': [file_2.id] }] } job_test_utils.create_job_exe(job=job_5, output=JobResults(job_5_output_dict)) # Complete job 5 and set its output so that update recipe message can give go ahead for child job 6 Job.objects.process_job_output([job_5.id], now()) job_6 = job_test_utils.create_job(job_type=job_type_4, status='PENDING', num_exes=0) self.recipe_4 = recipe_test_utils.create_recipe(recipe_type=self.recipe_type_3, input=data) recipe_test_utils.create_recipe_job(recipe=self.recipe_4, job_name='Job 1', job=job_5) recipe_test_utils.create_recipe_job(recipe=self.recipe_4, job_name='Job 2', job=job_6) # Add recipes to message message = UpdateRecipes() if message.can_fit_more(): message.add_recipe(self.recipe_1.id) if message.can_fit_more(): message.add_recipe(self.recipe_2.id) if message.can_fit_more(): message.add_recipe(self.recipe_3.id) if message.can_fit_more(): message.add_recipe(self.recipe_4.id) # Execute message result = message.execute() self.assertTrue(result) self.assertEqual(len(message.new_messages), 4) # Check message types blocked_jobs_msg = False pending_jobs_msg = False process_job_input_msg_job_3 = False process_job_input_msg_job_6 = False for new_msg in message.new_messages: if new_msg.type == 'blocked_jobs': blocked_jobs_msg = True elif new_msg.type == 'pending_jobs': pending_jobs_msg = True elif new_msg.type == 'process_job_input': if new_msg.job_id == job_3.id: process_job_input_msg_job_3 = True elif new_msg.job_id == job_6.id: process_job_input_msg_job_6 = True self.assertTrue(blocked_jobs_msg) self.assertTrue(pending_jobs_msg) self.assertTrue(process_job_input_msg_job_3) self.assertTrue(process_job_input_msg_job_6) # Make sure Job 3 has its input populated job = Job.objects.get(id=job_3.id) self.assertDictEqual(job.input, { 'version': '1.0', 'input_data': [{ 'name': input_name_1, 'file_id': file_1.id, }], 'output_data': [{ 'name': output_name_1, 'workspace_id': workspace.id, }], }) # Make sure Job 6 has its input populated job = Job.objects.get(id=job_6.id) self.assertDictEqual(job.input, { 'version': '1.0', 'input_data': [{ 'name': input_name_2, 'file_ids': [file_2.id], }], 'output_data': [{ 'name': output_name_2, 'workspace_id': workspace.id, }], }) # Test executing message again message_json_dict = message.to_json() message = UpdateRecipes.from_json(message_json_dict) result = message.execute() self.assertTrue(result) # Make sure the same three messages are returned self.assertEqual(len(message.new_messages), 4) # Check message types blocked_jobs_msg = False pending_jobs_msg = False process_job_input_msg_job_3 = False process_job_input_msg_job_6 = False for new_msg in message.new_messages: if new_msg.type == 'blocked_jobs': blocked_jobs_msg = True elif new_msg.type == 'pending_jobs': pending_jobs_msg = True elif new_msg.type == 'process_job_input': if new_msg.job_id == job_3.id: process_job_input_msg_job_3 = True elif new_msg.job_id == job_6.id: process_job_input_msg_job_6 = True self.assertTrue(blocked_jobs_msg) self.assertTrue(pending_jobs_msg) self.assertTrue(process_job_input_msg_job_3) self.assertTrue(process_job_input_msg_job_6)
def test_successful_job_1_completed(self, mock_store): '''Tests calling RecipeDefinition.get_next_jobs_to_queue() successfully when job 1 has been completed.''' definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'Job 1', 'job_type': { 'name': self.job_type_1.name, 'version': self.job_type_1.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': self.input_name_1, }] }, { 'name': 'Job 2', 'job_type': { 'name': self.job_type_2.name, 'version': self.job_type_2.version, }, 'dependencies': [{ 'name': 'Job 1', 'connections': [{ 'output': self.output_name_1, 'input': self.input_name_2, }], }], }], } recipe_definition = RecipeDefinition(definition) recipe_definition.validate_job_interfaces() data = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'file_id': self.file_1.id, }], 'workspace_id': 1, } recipe_data = RecipeData(data) recipe_definition.validate_data(recipe_data) png_file_ids = [98, 99, 100] job_results = JobResults() job_results.add_file_list_parameter(self.output_name_1, png_file_ids) job_1 = Job.objects.select_related('job_type').get(pk=self.job_1.id) job_1.results = job_results.get_dict() job_1.save() job_2 = Job.objects.select_related('job_type').get(pk=self.job_2.id) results = recipe_definition.get_next_jobs_to_queue(recipe_data, {'Job 2': job_2}, {'Job 1': job_1}) # Make sure only Job 2 is returned and that its job data is correct self.assertListEqual([self.job_2.id], results.keys()) self.assertDictEqual(results[self.job_2.id].get_dict(), { 'version': '1.0', 'input_data': [{ 'name': self.input_name_2, 'file_ids': png_file_ids, }], 'output_data': [{ 'name': self.output_name_2, 'workspace_id': 1, }], })
def test_process_job_output(self): """Tests calling JobManager.process_job_output()""" output_1 = JobResults() output_1.add_file_parameter('foo', 1) output_2 = JobResults() output_2.add_file_parameter('foo', 2) # These jobs have completed and have their execution results job_exe_1 = job_test_utils.create_job_exe(status='COMPLETED', output=output_1) job_exe_2 = job_test_utils.create_job_exe(status='COMPLETED', output=output_2) # These jobs have their execution results, but have not completed job_exe_3 = job_test_utils.create_job_exe(status='RUNNING') job_exe_4 = job_test_utils.create_job_exe(status='RUNNING') for job_exe in [job_exe_3, job_exe_4]: job_exe_output = JobExecutionOutput() job_exe_output.job_exe_id = job_exe.id job_exe_output.job_id = job_exe.job_id job_exe_output.job_type_id = job_exe.job.job_type_id job_exe_output.exe_num = job_exe.exe_num job_exe_output.output = JobResults().get_dict() job_exe_output.save() # These jobs have completed, but do not have their execution results job_exe_5 = job_test_utils.create_job_exe(status='RUNNING') job_exe_6 = job_test_utils.create_job_exe(status='RUNNING') for job in [job_exe_5.job, job_exe_6.job]: job.status = 'COMPLETED' job.save() # Test method job_ids = [ job_exe.job_id for job_exe in [job_exe_1, job_exe_2, job_exe_3, job_exe_4, job_exe_5, job_exe_6] ] result_ids = Job.objects.process_job_output(job_ids, timezone.now()) self.assertEqual(set(result_ids), {job_exe_1.job_id, job_exe_2.job_id}) # Jobs 1 and 2 should have output populated, jobs 3 through 6 should not jobs = list(Job.objects.filter(id__in=job_ids).order_by('id')) self.assertEqual(len(jobs), 6) self.assertTrue(jobs[0].has_output()) self.assertDictEqual(jobs[0].output, output_1.get_dict()) self.assertTrue(jobs[1].has_output()) self.assertDictEqual(jobs[1].output, output_2.get_dict()) self.assertFalse(jobs[2].has_output()) self.assertFalse(jobs[3].has_output()) self.assertFalse(jobs[4].has_output()) self.assertFalse(jobs[5].has_output())
import django from django.db.utils import DatabaseError, OperationalError from django.utils.timezone import now from django.test import TransactionTestCase from mock import patch from error.exceptions import ScaleDatabaseError, ScaleIOError, ScaleOperationalError from job.configuration.results.exceptions import InvalidResultsManifest, MissingRequiredOutput from job.configuration.results.job_results import JobResults from job.configuration.results.results_manifest.results_manifest import ResultsManifest from job.management.commands.scale_post_steps import Command as PostCommand from job.models import JobExecutionOutput from job.test import utils as job_utils from trigger.models import TriggerEvent JOB_RESULTS = JobResults() RESULTS_MANIFEST = ResultsManifest() RESULTS = (JOB_RESULTS, RESULTS_MANIFEST) class TestPostJobSteps(TransactionTestCase): def setUp(self): django.setup() cmd = 'command' cmd_args = 'args' interface = { 'version': '1.0', 'command': cmd,
def test_successful_job_1_completed(self, mock_store): '''Tests calling RecipeDefinition.get_next_jobs_to_queue() successfully when job 1 has been completed.''' definition = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'type': 'file', 'media_types': ['text/plain'], }], 'jobs': [{ 'name': 'Job 1', 'job_type': { 'name': self.job_type_1.name, 'version': self.job_type_1.version, }, 'recipe_inputs': [{ 'recipe_input': 'Recipe Input', 'job_input': self.input_name_1, }] }, { 'name': 'Job 2', 'job_type': { 'name': self.job_type_2.name, 'version': self.job_type_2.version, }, 'dependencies': [{ 'name': 'Job 1', 'connections': [{ 'output': self.output_name_1, 'input': self.input_name_2, }], }], }], } recipe_definition = RecipeDefinition(definition) recipe_definition.validate_job_interfaces() data = { 'version': '1.0', 'input_data': [{ 'name': 'Recipe Input', 'file_id': self.file_1.id, }], 'workspace_id': 1, } recipe_data = RecipeData(data) recipe_definition.validate_data(recipe_data) png_file_ids = [98, 99, 100] job_results = JobResults() job_results.add_file_list_parameter(self.output_name_1, png_file_ids) job_1 = Job.objects.select_related('job_type').get(pk=self.job_1.id) job_1.results = job_results.get_dict() job_1.save() job_2 = Job.objects.select_related('job_type').get(pk=self.job_2.id) results = recipe_definition.get_next_jobs_to_queue( recipe_data, {'Job 2': job_2}, {'Job 1': job_1}) # Make sure only Job 2 is returned and that its job data is correct self.assertListEqual([self.job_2.id], results.keys()) self.assertDictEqual( results[self.job_2.id].get_dict(), { 'version': '1.0', 'input_data': [{ 'name': self.input_name_2, 'file_ids': png_file_ids, }], 'output_data': [{ 'name': self.output_name_2, 'workspace_id': 1, }], })