コード例 #1
0
class SessionStateReactionToJobResultTests(TestCase):
    # This test checks how a simple session with a few typical job reacts to
    # job results of various kinds. It checks most of the resource presentation
    # error conditions that I could think of.

    def setUp(self):
        # All of the tests below are using one session. The session has four
        # jobs, clustered into two independent groups. Job A depends on a
        # resource provided by job R which has no dependencies at all. Job X
        # depends on job Y which in turn has no dependencies at all.
        #
        # A -(resource dependency)-> R
        #
        # X -(direct dependency) -> Y
        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='Y')
        self.job_Y = make_job("Y")
        self.job_list = [self.job_A, self.job_R, self.job_X, self.job_Y]
        self.session = SessionState(self.job_list)

    def job_state(self, id):
        # A helper function to avoid overly long expressions
        return self.session.job_state_map[id]

    def job_inhibitor(self, id, index):
        # Another helper that shortens deep object nesting
        return self.job_state(id).readiness_inhibitor_list[index]

    def test_assumptions(self):
        # This function checks the assumptions of SessionState initial state.
        # The job list is what we set when constructing the session.
        #
        self.assertEqual(self.session.job_list, self.job_list)
        # The run_list is still empty because the desired_job_list is equally
        # empty.
        self.assertEqual(self.session.run_list, [])
        self.assertEqual(self.session.desired_job_list, [])
        # All jobs have state objects that indicate they cannot run (because
        # they have the UNDESIRED inhibitor set for them by default).
        self.assertFalse(self.job_state('A').can_start())
        self.assertFalse(self.job_state('R').can_start())
        self.assertFalse(self.job_state('X').can_start())
        self.assertFalse(self.job_state('Y').can_start())
        self.assertEqual(
            self.job_inhibitor('A', 0).cause, InhibitionCause.UNDESIRED)
        self.assertEqual(
            self.job_inhibitor('R', 0).cause, InhibitionCause.UNDESIRED)
        self.assertEqual(
            self.job_inhibitor('X', 0).cause, InhibitionCause.UNDESIRED)
        self.assertEqual(
            self.job_inhibitor('Y', 0).cause, InhibitionCause.UNDESIRED)

    def test_desire_job_A_updates_state_map(self):
        # This function checks what happens when the job A becomes desired via
        # the update_desired_job_list() call.
        self.session.update_desired_job_list([self.job_A])
        self.assertEqual(self.session.desired_job_list, [self.job_A])
        # This should topologically sort the job list, according to the
        # relationship created by the resource requirement. This is not really
        # testing the dependency solver (it has separate tests), just that this
        # basic property is established and that the run_list properly shows
        # that R must run before A can run.
        self.assertEqual(self.session.run_list, [self.job_R, self.job_A])
        # This also recomputes job readiness state so that job R is no longer
        # undesired, has no other inhibitor and thus can start
        self.assertEqual(self.job_state('R').readiness_inhibitor_list, [])
        self.assertTrue(self.job_state('R').can_start())
        # While the A job still cannot run it now has a different inhibitor,
        # one with the PENDING_RESOURCE cause. The inhibitor also properly
        # pinpoints the related job and related expression.
        self.assertNotEqual(self.job_state('A').readiness_inhibitor_list, [])
        self.assertEqual(
            self.job_inhibitor('A', 0).cause, InhibitionCause.PENDING_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_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 = MemoryJobResult({
            'outcome': IJobResult.OUTCOME_PASS,
            'io_log': [(0, 'stdout', b"attr: value\n")],
        })
        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, InhibitionCause.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_normal_job_result_updates(self):
        # This function checks what happens when a JobResult for job A is
        # presented to the session. Set the outcome to a "different" value as
        # the initial job result was pretty much identical and the comparison
        # below would fail to work as the update would have been silently
        # ignored.
        result_A = MemoryJobResult({'outcome': 'different'})
        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, InhibitionCause.UNDESIRED)

    @mock.patch('plainbox.impl.ctrl.logger')
    def test_resource_job_with_broken_output(self, mock_logger):
        # 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 = MemoryJobResult({
            'outcome':
            IJobResult.OUTCOME_PASS,
            '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")],
        })
        # 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)

        # Make sure the right warning was logged
        mock_logger.warning.assert_called_once_with(
            "local script %s returned invalid RFC822 data: %s", self.job_R.id,
            RFC822SyntaxError(
                None, 3, "Unexpected non-empty line: "
                "'I-sound-like-a-broken-record\\n'"))

    def test_desire_job_X_updates_state_map(self):
        # This function checks what happens when the job X becomes desired via
        # the update_desired_job_list() call.
        self.session.update_desired_job_list([self.job_X])
        self.assertEqual(self.session.desired_job_list, [self.job_X])
        # As in the similar A - R test function above this topologically sorts
        # all affected jobs. Here X depends on Y so Y should be before X on the
        # run list.
        self.assertEqual(self.session.run_list, [self.job_Y, self.job_X])
        # As in the A - R test above this also recomputes the job readiness
        # state. Job Y is now runnable but job X has a PENDING_DEP inhibitor.
        self.assertEqual(self.job_state('Y').readiness_inhibitor_list, [])
        # While the A job still cannot run it now has a different inhibitor,
        # one with the PENDING_RESOURCE cause. The inhibitor also properly
        # pinpoints the related job and related expression.
        self.assertNotEqual(self.job_state('X').readiness_inhibitor_list, [])
        self.assertEqual(
            self.job_inhibitor('X', 0).cause, InhibitionCause.PENDING_DEP)
        self.assertEqual(self.job_inhibitor('X', 0).related_job, self.job_Y)
        self.assertFalse(self.job_state('X').can_start())

    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, InhibitionCause.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 = MemoryJobResult({'outcome': IJobResult.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, InhibitionCause.FAILED_DEP)
        self.assertEqual(self.job_inhibitor('X', 0).related_job, self.job_Y)
        self.assertFalse(self.job_state('X').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 = MemoryJobResult({'outcome': IJobResult.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 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 = MemoryJobResult({
            'outcome':
            IJobResult.OUTCOME_PASS,
            'io_log': [(0, 'stdout', b'attr: wrong value\n')],
        })
        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, InhibitionCause.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_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 = MemoryJobResult({
            'outcome':
            IJobResult.OUTCOME_PASS,
            'io_log': [(0, 'stdout', b"attr: old value\n")]
        })
        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 = MemoryJobResult({
            'outcome':
            IJobResult.OUTCOME_PASS,
            'io_log': [(0, 'stdout', b"attr: new value\n")]
        })
        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_get_outcome_stats(self):
        result_A = MemoryJobResult({'outcome': IJobResult.OUTCOME_PASS})
        result_R = MemoryJobResult({'outcome': IJobResult.OUTCOME_FAIL})
        result_Y = MemoryJobResult({'outcome': IJobResult.OUTCOME_FAIL})
        self.session.update_job_result(self.job_A, result_A)
        self.session.update_job_result(self.job_R, result_R)
        self.session.update_job_result(self.job_Y, result_Y)
        self.assertEqual(self.session.get_outcome_stats(), {
            IJobResult.OUTCOME_PASS: 1,
            IJobResult.OUTCOME_FAIL: 2
        })

    def test_get_certification_status_map(self):
        result_A = MemoryJobResult({'outcome': IJobResult.OUTCOME_PASS})
        self.session.update_job_result(self.job_A, result_A)
        self.session.job_state_map[
            self.job_A.id].effective_certification_status = 'foo'
        self.assertEqual(self.session.get_certification_status_map(), {})
        self.assertEqual(
            self.session.get_certification_status_map(
                outcome_filter=(IJobResult.OUTCOME_PASS, ),
                certification_status_filter=('foo', )),
            {self.job_A.id: self.session.job_state_map[self.job_A.id]})
        result_Y = MemoryJobResult({'outcome': IJobResult.OUTCOME_FAIL})
        self.session.job_state_map[
            self.job_Y.id].effective_certification_status = 'bar'
        self.assertEqual(self.session.get_certification_status_map(), {})
        self.assertEqual(
            self.session.get_certification_status_map(
                outcome_filter=(IJobResult.OUTCOME_PASS,
                                IJobResult.OUTCOME_FAIL),
                certification_status_filter=('foo', 'bar')),
            {self.job_A.id: self.session.job_state_map[self.job_A.id]})
        self.session.update_job_result(self.job_Y, result_Y)
        self.assertEqual(
            self.session.get_certification_status_map(
                outcome_filter=(IJobResult.OUTCOME_PASS,
                                IJobResult.OUTCOME_FAIL),
                certification_status_filter=('foo', 'bar')), {
                    self.job_A.id: self.session.job_state_map[self.job_A.id],
                    self.job_Y.id: self.session.job_state_map[self.job_Y.id]
                })