def test_resource_job_result_overwrites_old_resources(self): # This function checks what happens when a JobResult for job R is # presented to a session that has some resources from that job already. result_R_old = JobResult({ 'job': self.job_R, 'io_log': make_io_log(((0, 'stdout', b"attr: old value\n"), ), self.scratch_dir) }) self.session.update_job_result(self.job_R, result_R_old) # So here the old result is stored into a new 'R' resource expected_before = {'R': [Resource({'attr': 'old value'})]} self.assertEqual(self.session._resource_map, expected_before) # Now we present the second result for the same job result_R_new = JobResult({ 'job': self.job_R, 'io_log': make_io_log(((0, 'stdout', b"attr: new value\n"), ), self.scratch_dir) }) self.session.update_job_result(self.job_R, result_R_new) # What should happen here is that the R resource is entirely replaced # by the data from the new result. The data should not be merged or # appended in any way. expected_after = {'R': [Resource({'attr': 'new value'})]} self.assertEqual(self.session._resource_map, expected_after)
def test_resume_session(self): # All of the tests below are using one session. The session has four # jobs, Job A depends on a resource provided by job R which has no # dependencies at all. Both Job X and Y depend on job A. # # A -(resource dependency)-> R # # X -(direct dependency) -> A # # Y -(direct dependency) -> A self.job_A = make_job("A", requires="R.attr == 'value'") self.job_A_expr = self.job_A.get_resource_program().expression_list[0] self.job_R = make_job("R", plugin="resource") self.job_X = make_job("X", depends='A') self.job_Y = make_job("Y", depends='A') self.job_list = [self.job_A, self.job_R, self.job_X, self.job_Y] # Create a new session (session_dir is empty) self.session = SessionState(self.job_list) result_R = JobResult({ 'job': self.job_R, 'io_log': make_io_log(((0, 'stdout', b"attr: value\n"), ), self._sandbox) }) result_A = JobResult({ 'job': self.job_A, 'outcome': JobResult.OUTCOME_PASS }) result_X = JobResult({ 'job': self.job_X, 'outcome': JobResult.OUTCOME_PASS }) # Job Y can't start as it requires job A self.assertFalse(self.job_state('Y').can_start()) self.session.update_desired_job_list([self.job_X, self.job_Y]) self.session.open() self.session.update_job_result(self.job_R, result_R) self.session.update_job_result(self.job_A, result_A) self.session.update_job_result(self.job_X, result_X) self.session.persistent_save() self.session.close() # Create a new session (session_dir should contain session data) self.session = SessionState(self.job_list) self.session.open() # Resume the previous session self.session.resume() # This time job Y can start self.assertTrue(self.job_state('Y').can_start()) self.session.close()
def test_encode(self): result = JobResult({ 'job': self.job, 'outcome': JobResult.OUTCOME_PASS, 'comments': "it said blah", 'io_log': ((0, 'stdout', 'blah\n'),), 'return_code': 0 }) result_enc = result._get_persistance_subset() self.assertEqual(result_enc['data']['job'], result.job) self.assertEqual(result_enc['data']['outcome'], result.outcome) self.assertEqual(result_enc['data']['comments'], result.comments) self.assertEqual(result_enc['data']['return_code'], result.return_code) with self.assertRaises(KeyError): result_enc['io_log']
def test_desired_job_X_cannot_run_with_failed_job_Y(self): # This function checks how SessionState reacts when the desired job X # readiness state changes when presented with a failed result to job Y self.session.update_desired_job_list([self.job_X]) # When X is desired, as above, it should be inhibited with PENDING_DEP # on Y self.assertNotEqual(self.job_state('X').readiness_inhibitor_list, []) self.assertEqual( self.job_inhibitor('X', 0).cause, JobReadinessInhibitor.PENDING_DEP) self.assertEqual(self.job_inhibitor('X', 0).related_job, self.job_Y) self.assertFalse(self.job_state('X').can_start()) # When a failed Y result is presented X should switch to FAILED_DEP result_Y = JobResult({ 'job': self.job_Y, 'outcome': JobResult.OUTCOME_FAIL }) self.session.update_job_result(self.job_Y, result_Y) # Now job X should have a FAILED_DEP inhibitor instead of the # PENDING_DEP it had before. Everything else should stay as-is. self.assertNotEqual(self.job_state('X').readiness_inhibitor_list, []) self.assertEqual( self.job_inhibitor('X', 0).cause, JobReadinessInhibitor.FAILED_DEP) self.assertEqual(self.job_inhibitor('X', 0).related_job, self.job_Y) self.assertFalse(self.job_state('X').can_start())
def test_resource_job_with_broken_output(self): # This function checks how SessionState parses partially broken # resource jobs. A JobResult with broken output is constructed below. # The output will describe one proper record, one broken record and # another proper record in that order. result_R = JobResult({ 'job': self.job_R, 'io_log': make_io_log( ((0, 'stdout', b"attr: value-1\n"), (1, 'stdout', b"\n"), (1, 'stdout', b"I-sound-like-a-broken-record\n"), (1, 'stdout', b"\n"), (1, 'stdout', b"attr: value-2\n")), self.scratch_dir) }) # Since we cannot control the output of scripts and people indeed make # mistakes a warning is issued but no exception is raised to the # caller. self.session.update_job_result(self.job_R, result_R) # The observation here is that the parser is not handling the exception # in away which would allow for recovery. Out of all the output only # the first record is created and stored properly. The third, proper # record is entirely ignored. expected = {'R': [Resource({'attr': 'value-1'})]} self.assertEqual(self.session._resource_map, expected)
def test_resource_job_result_updates_resource_and_job_states(self): # This function checks what happens when a JobResult for job R (which # is a resource job via the resource plugin) is presented to the # session. result_R = JobResult({ 'job': self.job_R, 'io_log': make_io_log(((0, 'stdout', b"attr: value\n"), ), self.scratch_dir) }) self.session.update_job_result(self.job_R, result_R) # The most obvious thing that can happen, is that the result is simply # stored in the associated job state object. self.assertIs(self.job_state('R').result, result_R) # Initially the _resource_map was empty. SessionState parses the io_log # of results of resource jobs and creates appropriate resource objects. self.assertIn("R", self.session._resource_map) expected = {'R': [Resource({'attr': 'value'})]} self.assertEqual(self.session._resource_map, expected) # As job results are presented to the session the readiness of other # jobs is changed. Since A depends on R via a resource expression and # the particular resource that were produced by R in this test should # allow the expression to match the readiness inhibitor from A should # have been removed. Since this test does not use # update_desired_job_list() a will still have the UNDESIRED inhibitor # but it will no longer have the PENDING_RESOURCE inhibitor, self.assertEqual( self.job_inhibitor('A', 0).cause, JobReadinessInhibitor.UNDESIRED) # Now if we put A on the desired list this should clear the UNDESIRED # inhibitor and make A runnable. self.session.update_desired_job_list([self.job_A]) self.assertTrue(self.job_state('A').can_start())
def test_desired_job_X_can_run_with_passing_job_Y(self): # A variant of the test case above, simply Y passes this time, making X # runnable self.session.update_desired_job_list([self.job_X]) result_Y = JobResult({ 'job': self.job_Y, 'outcome': JobResult.OUTCOME_PASS }) self.session.update_job_result(self.job_Y, result_Y) # Now X is runnable self.assertEqual(self.job_state('X').readiness_inhibitor_list, []) self.assertTrue(self.job_state('X').can_start())
def __init__(self, job): """ Initialize a new job state object. The job will be inhibited by a single UNDESIRED inhibitor and will have a result with OUTCOME_NONE that basically says it did not run yet. """ self._job = job self._readiness_inhibitor_list = [UndesiredJobReadinessInhibitor] self._result = JobResult({ 'job': job, 'outcome': JobResult.OUTCOME_NONE })
def test_encode_normal_job(self): result = JobResult({ 'job': self.job, 'outcome': JobResult.OUTCOME_PASS, }) self.job_state.result = result jobstate_enc = self.job_state._get_persistance_subset() # The inhibitor list is not saved with self.assertRaises(KeyError): jobstate_enc['_readiness_inhibitor_list'] # Normal jobs should keep their outcome value self.assertEqual(jobstate_enc['_result'].outcome, JobResult.OUTCOME_PASS)
def _get_persistance_subset(self): # Don't save resource job results, fresh data are required # so we can't reuse the old ones # The inhibitor list needs to be recomputed as well, don't save it. state = {} state['_job'] = self._job if self._job.plugin == 'resource': state['_result'] = JobResult({ 'job': self._job, 'outcome': JobResult.OUTCOME_NONE }) else: state['_result'] = self._result return state
def test_normal_job_result_updates(self): # This function checks what happens when a JobResult for job A is # presented to the session. result_A = JobResult({'job': self.job_A}) self.session.update_job_result(self.job_A, result_A) # As before the result should be stored as-is self.assertIs(self.job_state('A').result, result_A) # Unlike before _resource_map should be left unchanged self.assertEqual(self.session._resource_map, {}) # One interesting observation is that readiness inhibitors are entirely # unaffected by existing test results beyond dependency and resource # relationships. While a result for job A was presented, job A is still # inhibited by the UNDESIRED inhibitor. self.assertEqual( self.job_inhibitor('A', 0).cause, JobReadinessInhibitor.UNDESIRED)
def test_encode_resource_job(self): self.job_R = make_job("R", plugin="resource") result_R = JobResult({ 'job': self.job_R, 'outcome': JobResult.OUTCOME_PASS, 'io_log': ((0, 'stdout', "attr: value\n"), ) }) jobstate = JobState(self.job_R) jobstate.result = result_R jobstate_enc = jobstate._get_persistance_subset() # The inhibitor list is not saved with self.assertRaises(KeyError): jobstate_enc['_readiness_inhibitor_list'] # Resource have to be re evealutated on startup, outcome of the job # must be reset to JobResult.OUTCOME_NONE self.assertEqual(jobstate_enc['_result'].outcome, JobResult.OUTCOME_NONE)
def test_desired_job_X_cannot_run_with_no_resource_R(self): # A variant of the two test cases above, using A-R jobs self.session.update_desired_job_list([self.job_A]) result_R = JobResult({ 'job': self.job_R, 'io_log': make_io_log(((0, 'stdout', b'attr: wrong value\n'), ), self.scratch_dir) }) self.session.update_job_result(self.job_R, result_R) # Now A is inhibited by FAILED_RESOURCE self.assertNotEqual(self.job_state('A').readiness_inhibitor_list, []) self.assertEqual( self.job_inhibitor('A', 0).cause, JobReadinessInhibitor.FAILED_RESOURCE) self.assertEqual(self.job_inhibitor('A', 0).related_job, self.job_R) self.assertEqual( self.job_inhibitor('A', 0).related_expression, self.job_A_expr) self.assertFalse(self.job_state('A').can_start())
def test_persistent_save(self): self.job_A = make_job("A") self.job_list = [self.job_A] self.session = SessionState(self.job_list) result_A = JobResult({ 'job': self.job_A, 'outcome': JobResult.OUTCOME_PASS, 'comments': 'All good', 'return_code': 0, 'io_log': ((0, 'stdout', "Success !\n"), ) }) session_json_text = """{ "_job_state_map": { "A": { "_job": { "data": { "name": "A", "plugin": "dummy", "requires": null, "depends": null }, "_class_id": "JOB_DEFINITION" }, "_result": { "data": { "job": { "data": { "name": "A", "plugin": "dummy", "requires": null, "depends": null }, "_class_id": "JOB_DEFINITION" }, "outcome": "pass", "return_code": 0, "comments": "All good", "io_log": [ [ 0, "stdout", "Success !\\n" ] ] }, "_class_id": "JOB_RESULT" }, "_class_id": "JOB_STATE" } }, "_desired_job_list": [ { "data": { "name": "A", "plugin": "dummy", "requires": null, "depends": null }, "_class_id": "JOB_DEFINITION" } ], "_class_id": "SESSION_STATE" }""" self.session.open() self.session.update_desired_job_list([self.job_A]) self.session.update_job_result(self.job_A, result_A) self.session.persistent_save() session_file = self.session.previous_session_file() self.session.close() self.assertIsNotNone(session_file) with open(session_file) as f: raw_json = json.load(f) self.maxDiff = None self.assertEqual(raw_json, json.loads(session_json_text))