def test_dependency_cycle_self(self): # This tests dependency loops # A -> A A = make_job(id='A', depends='A') job_list = [A] with self.assertRaises(DependencyCycleError) as call: DependencySolver.resolve_dependencies(job_list) self.assertEqual(call.exception.job_list, [A, A])
def test_duplicate_error(self): A = make_job('A') another_A = make_job('A') job_list = [A, another_A] with self.assertRaises(DependencyDuplicateError) as call: DependencySolver.resolve_dependencies(job_list) self.assertIs(call.exception.job, A) self.assertIs(call.exception.duplicate_job, another_A)
def test_dependency_cycle_self(self): # This tests dependency loops # A -> A A = make_job(name='A', depends='A') job_list = [A] with self.assertRaises(DependencyCycleError) as call: DependencySolver.resolve_dependencies(job_list) self.assertEqual(call.exception.job_list, [A, A])
def test_dependency_cycle_via_resource(self): # This tests dependency loops # A -> R -> A A = make_job(id='A', requires='R.key == "value"') R = make_job(id='R', depends='A', plugin="resource") job_list = [A, R] with self.assertRaises(DependencyCycleError) as call: DependencySolver.resolve_dependencies(job_list) self.assertEqual(call.exception.job_list, [A, R, A])
def test_dependency_cycle_via_resource(self): # This tests dependency loops # A -> R -> A A = make_job(name='A', requires='R.key == "value"') R = make_job(name='R', depends='A', plugin="resource") job_list = [A, R] with self.assertRaises(DependencyCycleError) as call: DependencySolver.resolve_dependencies(job_list) self.assertEqual(call.exception.job_list, [A, R, A])
def test_missing_direct_dependency(self): # This tests missing dependencies # A -> (inexisting B) A = make_job(id='A', depends='B') job_list = [A] with self.assertRaises(DependencyMissingError) as call: DependencySolver.resolve_dependencies(job_list) self.assertIs(call.exception.job, A) self.assertEqual(call.exception.missing_job_id, 'B') self.assertEqual(call.exception.dep_type, call.exception.DEP_TYPE_DIRECT)
def test_missing_resource_dependency(self): # This tests missing resource dependencies # A ~> (inexisting R) A = make_job(id='A', requires='R.attr == "value"') job_list = [A] with self.assertRaises(DependencyMissingError) as call: DependencySolver.resolve_dependencies(job_list) self.assertIs(call.exception.job, A) self.assertEqual(call.exception.missing_job_id, 'R') self.assertEqual(call.exception.dep_type, call.exception.DEP_TYPE_RESOURCE)
def test_missing_direct_dependency(self): # This tests missing dependencies # A -> (inexisting B) A = make_job(name='A', depends='B') job_list = [A] with self.assertRaises(DependencyMissingError) as call: DependencySolver.resolve_dependencies(job_list) self.assertIs(call.exception.job, A) self.assertEqual(call.exception.missing_job_name, 'B') self.assertEqual(call.exception.dep_type, call.exception.DEP_TYPE_DIRECT)
def test_dependency_cycle_longer(self): # This tests dependency loops # A -> B -> C -> D -> B A = make_job(name='A', depends='B') B = make_job(name='B', depends='C') C = make_job(name='C', depends='D') D = make_job(name='D', depends='B') job_list = [A, B, C, D] with self.assertRaises(DependencyCycleError) as call: DependencySolver.resolve_dependencies(job_list) self.assertEqual(call.exception.job_list, [B, C, D, B])
def test_dependency_cycle_longer(self): # This tests dependency loops # A -> B -> C -> D -> B A = make_job(id='A', depends='B') B = make_job(id='B', depends='C') C = make_job(id='C', depends='D') D = make_job(id='D', depends='B') job_list = [A, B, C, D] with self.assertRaises(DependencyCycleError) as call: DependencySolver.resolve_dependencies(job_list) self.assertEqual(call.exception.job_list, [B, C, D, B])
def test_missing_resource_dependency(self): # This tests missing resource dependencies # A ~> (inexisting R) A = make_job(name='A', requires='R.attr == "value"') job_list = [A] with self.assertRaises(DependencyMissingError) as call: DependencySolver.resolve_dependencies(job_list) self.assertIs(call.exception.job, A) self.assertEqual(call.exception.missing_job_name, 'R') self.assertEqual(call.exception.dep_type, call.exception.DEP_TYPE_RESOURCE)
def test_resource_deps(self): # This tests resource deps # A ~> R A = make_job(name='A', requires='R.foo == "bar"') R = make_job(name='R', plugin='resource') job_list = [A, R] expected = [R, A] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_resource_deps(self): # This tests resource deps # A ~> R A = make_job(id='A', requires='R.foo == "bar"') R = make_job(id='R', plugin='resource') job_list = [A, R] expected = [R, A] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_direct_deps(self): # This tests the following simple job chain # A -> B -> C A = make_job(id='A', depends='B') B = make_job(id='B', depends='C') C = make_job(id='C') job_list = [A, B, C] expected = [C, B, A] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_direct_deps(self): # This tests the following simple job chain # A -> B -> C A = make_job(name='A', depends='B') B = make_job(name='B', depends='C') C = make_job(name='C') job_list = [A, B, C] expected = [C, B, A] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_visiting_blackend_node(self): # This tests a visit to already visited job # A # B -> A # A will be visited twice A = make_job(name='A') B = make_job(name='B', depends='A') job_list = [A, B] expected = [A, B] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_visiting_blackend_node(self): # This tests a visit to already visited job # A # B -> A # A will be visited twice A = make_job(id='A') B = make_job(id='B', depends='A') job_list = [A, B] expected = [A, B] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_independent_groups_deps(self): # This tests two independent job chains # A1 -> B1 # A2 -> B2 A1 = make_job(name='A1', depends='B1') B1 = make_job(name='B1',) A2 = make_job(name='A2', depends='B2') B2 = make_job(name='B2') job_list = [A1, B1, A2, B2] expected = [B1, A1, B2, A2] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def test_independent_groups_deps(self): # This tests two independent job chains # A1 -> B1 # A2 -> B2 A1 = make_job(id='A1', depends='B1') B1 = make_job(id='B1',) A2 = make_job(id='A2', depends='B2') B2 = make_job(id='B2') job_list = [A1, B1, A2, B2] expected = [B1, A1, B2, A2] observed = DependencySolver.resolve_dependencies(job_list) self.assertEqual(expected, observed)
def update_desired_job_list(self, desired_job_list): """ Update the set of desired jobs (that ought to run) This method can be used by the UI to recompute the dependency graph. The argument 'desired_job_list' is a list of jobs that should run. Those jobs must be a sub-collection of the job_list argument that was passed to the constructor. It never fails although it may reduce the actual permitted desired_job_list to an empty list. It returns a list of problems (all instances of DependencyError class), one for each job that had to be removed. """ # Remember a copy of original desired job list. We may modify this list # so let's not mess up data passed by the caller. self._desired_job_list = list(desired_job_list) # Reset run list just in case desired_job_list is empty self._run_list = [] # Try to solve the dependency graph. This is done in a loop as may need # to remove a problematic job and re-try. The loop provides a stop # condition as we will eventually run out of jobs. problems = [] while self._desired_job_list: # XXX: it might be more efficient to incorporate this 'recovery # mode' right into the solver, this way we'd probably save some # resources or runtime complexity. try: self._run_list = DependencySolver.resolve_dependencies( self._job_list, self._desired_job_list) except DependencyError as exc: # When a dependency error is detected remove the affected job # form _desired_job_list and try again. self._desired_job_list.remove(exc.affected_job) # Remember each problem, this can be presented by the UI problems.append(exc) continue else: # Don't iterate the loop if there was no exception break # Update all job readiness state self._recompute_job_readiness() # Return all dependency problems to the caller return problems
def test_empty(self): observed = DependencySolver.resolve_dependencies([]) expected = [] self.assertEqual(expected, observed)