def test_delitem(self): res = Resource() with self.assertRaises(KeyError): del res["attr"] res = Resource({'attr': 'value'}) del res['attr'] self.assertRaises(KeyError, lambda res: res['attr'], res)
def _populate_session_state(self, job, state): io_log = [ IOLogRecord(count, 'stdout', line.encode('utf-8')) for count, line in enumerate( job.get_record_value('io_log').splitlines(keepends=True)) ] result = MemoryJobResult({ 'outcome': job.get_record_value('outcome', job.get_record_value('status')), 'comments': job.get_record_value('comments'), 'execution_duration': job.get_record_value('duration'), 'io_log': io_log, }) state.update_job_result(job, result) if job.plugin == 'resource': new_resource_list = [] for record in gen_rfc822_records_from_io_log(job, result): resource = Resource(record.data) new_resource_list.append(resource) if not new_resource_list: new_resource_list = [Resource({})] state.set_resource_list(job.id, new_resource_list) job_state = state.job_state_map[job.id] job_state.effective_category_id = job.get_record_value( 'category_id', 'com.canonical.plainbox::uncategorised') job_state.effective_certification_status = job.get_record_value( 'certification_status', 'unspecified')
def test_delattr(self): res = Resource() self.assertRaises(AttributeError, delattr, res, "_attr") res = Resource({'attr': 'value'}) del res.attr self.assertRaises(AttributeError, getattr, res, "attr") self.assertRaises(AttributeError, lambda res: res.attr, res)
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_evaluate_success(self): resource_map = { 'package': [Resource({'name': 'plainbox'}), Resource({'name': 'fwts'})], 'platform': [Resource({'arch': 'i386'})] } self.assertTrue(self.prog.evaluate_or_raise(resource_map))
def test_should_instantiate__no_filter(self): template = TemplateUnit({ 'template-resource': 'resource', }) self.assertTrue( template.should_instantiate(Resource({'attr': 'value'}))) self.assertTrue( template.should_instantiate(Resource({'attr': 'other value'}))) self.assertTrue(template.should_instantiate(Resource()))
def test_compound_expression_or_passing(self): resource_map = { 'a': [Resource({'foo': 1})], 'b': [Resource({'bar': 3})] } expr = ResourceExpression("a.foo == 1 or b.bar == 2") self.assertTrue( expr.evaluate(resource_map['a'], resource_map['b'], resource_map=resource_map))
def test_compound_expression_or_failing(self): resource_map = { 'a': [Resource({'foo': 2})], 'b': [Resource({'bar': 3})] } expr = ResourceExpression("a.foo == 1 and b.bar == 2") self.assertFalse( expr.evaluate(resource_map['a'], resource_map['b'], resource_map=resource_map))
def test_evaluate_failure_not_true(self): resource_map = { 'package': [ Resource({'name': 'plainbox'}), ], 'platform': [Resource({'arch': 'i386'})] } with self.assertRaises(ExpressionFailedError) as call: self.prog.evaluate_or_raise(resource_map) self.assertEqual(call.exception.expression.text, "package.name == 'fwts'")
def test_evaluate(self): resources = { 'package': [Resource({'name': 'plainbox'}), Resource({'name': 'fwts'})], 'platform': [Resource({'arch': 'i386'})] } self.assertIn('package', resources) self.assertIn('platform', resources) for name in self.prog.required_resources: self.assertIn(name, resources) self.assertTrue(self.prog.evaluate(resources))
def test_compound_many_subexpressions_failing(self): resource_map = { 'a': [Resource({'foo': 1}), Resource({'foo': 2})], 'b': [Resource({'bar': 3}), Resource({'bar': 4})] } expr = ResourceExpression( "a.foo == 3 or b.bar == 3 and a.foo == 2 and b.bar == 1") self.assertFalse( expr.evaluate(resource_map['a'], resource_map['b'], resource_map=resource_map))
def setUp(self): self.exporter_unit = self._get_all_exporter_units()[ '2013.com.canonical.plainbox::html'] self.resource_map = { '2013.com.canonical.certification::lsb': [ Resource({'description': 'Ubuntu 14.04 LTS'})], '2013.com.canonical.certification::package': [ Resource({'name': 'plainbox', 'version': '1.0'}), Resource({'name': 'fwts', 'version': '0.15.2'})], } self.job1 = JobDefinition({'id': 'job_id1', '_summary': 'job 1'}) self.job2 = JobDefinition({'id': 'job_id2', '_summary': 'job 2'}) self.job3 = JobDefinition({'id': 'job_id3', '_summary': 'job 3'}) self.result_fail = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_FAIL, 'return_code': 1, 'io_log': [(0, 'stderr', b'FATAL ERROR\n')], }) self.result_pass = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_PASS, 'return_code': 0, 'io_log': [(0, 'stdout', b'foo\n')], 'comments': 'blah blah' }) self.result_skip = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_SKIP, 'comments': 'No such device' }) self.attachment = JobDefinition({ 'id': 'dmesg_attachment', 'plugin': 'attachment'}) self.attachment_result = MemoryJobResult({ 'outcome': IJobResult.OUTCOME_PASS, 'io_log': [(0, 'stdout', b'bar\n')], 'return_code': 0 }) self.session_manager = SessionManager.create() self.session_manager.add_local_device_context() self.session_state = self.session_manager.default_device_context.state session_state = self.session_state session_state.add_unit(self.job1) session_state.add_unit(self.job2) session_state.add_unit(self.job3) session_state.add_unit(self.attachment) session_state.update_job_result(self.job1, self.result_fail) session_state.update_job_result(self.job2, self.result_pass) session_state.update_job_result(self.job3, self.result_skip) session_state.update_job_result( self.attachment, self.attachment_result) for resource_id, resource_list in self.resource_map.items(): session_state.set_resource_list(resource_id, resource_list)
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, 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 _plugin_resource(self, job): proc = self._run_command(job) if proc.returncode == 127: logging.warning("Unable to find command: %s", job.command) return line_list = [] for byte_line in proc.stdout.splitlines(): try: line = byte_line.decode("UTF-8") except UnicodeDecodeError as exc: logger.warning("resource script %s returned invalid UTF-8 data" " %r: %s", job, byte_line, exc) else: line_list.append(line) with StringIO("\n".join(line_list)) as stream: try: record_list = load_rfc822_records(stream) except RFC822SyntaxError as exc: logger.warning("resource script %s returned invalid RFC822" " data: %s", job, exc) else: for record in record_list: logger.info("Storing resource record %s: %s", job.name, record) resource = Resource(record) self._context.add_resource(job.name, resource)
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_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_trim_does_remove_resources(self): """ verify that trim_job_list() removes resources for removed jobs """ self.session.set_resource_list("a", [Resource({'attr': 'value'})]) self.assertIn("a", self.session.resource_map) self.session.trim_job_list(JobIdQualifier("a")) self.assertNotIn("a", self.session.resource_map)
def test_set_resource_list(self): # Define an empty session session = SessionState([]) # Define a resource old_res = Resource({'attr': 'old value'}) # Set the resource list with the old resource # So here the old result is stored into a new 'R' resource session.set_resource_list('R', [old_res]) # Ensure that it worked self.assertEqual(session._resource_map, {'R': [old_res]}) # Define another resource new_res = Resource({'attr': 'new value'}) # Now we present the second result for the same job session.set_resource_list('R', [new_res]) # 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. self.assertEqual(session._resource_map, {'R': [new_res]})
def test_observe_result__resource(self): job = mock.Mock(spec=JobDefinition, plugin='resource') result = mock.Mock(spec=IJobResult, outcome=IJobResult.OUTCOME_PASS) result.get_io_log.return_value = [(0, 'stdout', b'attr: value1\n'), (0, 'stdout', b'\n'), (0, 'stdout', b'attr: value2\n')] session_state = mock.MagicMock(spec=SessionState) self.ctrl.observe_result(session_state, job, result) # Ensure that result got stored self.assertIs(session_state.job_state_map[job.id].result, result) # Ensure that signals got fired session_state.on_job_state_map_changed.assert_called_once_with() session_state.on_job_result_changed.assert_called_once_with( job, result) # Ensure that new resource was defined session_state.set_resource_list.assert_called_once_with( job.id, [Resource({'attr': 'value1'}), Resource({'attr': 'value2'})])
def test_get_inhibitor_list_good_resource(self): # verify that jobs that require a resource that has been invoked and # produced resources for which the expression evaluates to True don't # have any inhibitors j1 = JobDefinition({'id': 'j1', 'requires': 'j2.attr == "ok"'}) j2 = JobDefinition({'id': 'j2'}) session_state = mock.MagicMock(spec=SessionState) session_state.resource_map = {'j2': [Resource({'attr': 'ok'})]} session_state.job_state_map['j2'].job = j2 self.assertEqual(self.ctrl.get_inhibitor_list(session_state, j1), [])
def _process_resource_result(self, result): new_resource_list = [] for record in self._gen_rfc822_records_from_io_log(result): # XXX: Consider forwarding the origin object here. I guess we # should have from_frc822_record as with JobDefinition resource = Resource(record.data) logger.info("Storing resource record %r: %s", result.job.name, resource) new_resource_list.append(resource) # Replace any old resources with the new resource list self._resource_map[result.job.name] = new_resource_list
def _parse_and_store_resource(self, session_state, job, result): # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928 # If we are resuming from a session that had a resource job that # never ran, we will see an empty MemoryJobResult object. # Processing empty I/O log would create an empty resource list # and that state is different from the state the session started # before it was suspended, so don't if result.outcome is IJobResult.OUTCOME_NONE: return new_resource_list = [] for record in gen_rfc822_records_from_io_log(job, result): # XXX: Consider forwarding the origin object here. I guess we # should have from_frc822_record as with JobDefinition resource = Resource(record.data) logger.info(_("Storing resource record %r: %s"), job.id, resource) new_resource_list.append(resource) # Create an empty resource object to properly fail __getattr__ calls if not new_resource_list: new_resource_list = [Resource({})] # Replace any old resources with the new resource list session_state.set_resource_list(job.id, new_resource_list)
def _make_cert_resources(self): # Create some specific resources that this exporter relies on. The # corresponding jobs are _not_ loaded but this is irrelevant. state = self.manager.default_device_context.state ns = CERTIFICATION_NS state.set_resource_list( ns + 'cpuinfo', [ Resource({ 'PROP-1': 'VALUE-1', 'PROP-2': 'VALUE-2', 'count': '2', # NOTE: this has to be a number :/ }) ]) state.set_resource_list( ns + 'dpkg', [Resource({ 'architecture': 'dpkg.ARCHITECTURE', })]) state.set_resource_list(ns + 'lsb', [ Resource({ 'codename': 'lsb.CODENAME', 'description': 'lsb.DESCRIPTION', 'release': 'lsb.RELEASE', 'distributor_id': 'lsb.DISTRIBUTOR_ID', }) ]) state.set_resource_list(ns + 'uname', [Resource({ 'release': 'uname.RELEASE', })]) state.set_resource_list(ns + 'package', [ Resource({ 'name': 'package.0.NAME', 'version': 'package.0.VERSION', }), Resource({ 'name': 'package.1.NAME', 'version': 'package.1.VERSION', }) ]) state.set_resource_list(ns + 'requirements', [ Resource({ 'name': 'requirement.0.NAME', 'link': 'requirement.0.LINK', }), Resource({ 'name': 'requirement.1.NAME', 'link': 'requirement.1.LINK', }) ])
def test_get_inhibitor_list_FAILED_RESOURCE(self): # verify that jobs that require a resource that has been # invoked and produced resources but the expression dones't # evaluate to True produce the FAILED_RESOURCE inhibitor j1 = JobDefinition({'id': 'j1', 'requires': 'j2.attr == "ok"'}) j2 = JobDefinition({'id': 'j2'}) session_state = mock.MagicMock(spec=SessionState) session_state.job_state_map['j2'].job = j2 session_state.resource_map = {'j2': [Resource({'attr': 'not-ok'})]} self.assertEqual(self.ctrl.get_inhibitor_list(session_state, j1), [ JobReadinessInhibitor(JobReadinessInhibitor.FAILED_RESOURCE, j2, ResourceExpression('j2.attr == "ok"')) ])
def _process_resource_result(self, session_state, job, result, fake_resources=False): """ Analyze a result of a CheckBox "resource" job and generate or replace resource records. """ self._parse_and_store_resource(session_state, job, result) if session_state.resource_map[job.id] != [Resource({})]: self._instantiate_templates(session_state, job, result, fake_resources)
def test_instantiate_all(self): template = TemplateUnit({ 'template-resource': 'resource', 'template-filter': 'resource.attr == "value"', 'id': 'check-device-{dev_name}', 'summary': 'Test {name} ({sys_path})', 'plugin': 'shell', }) unit_list = template.instantiate_all([ Resource({ 'attr': 'value', 'dev_name': 'sda1', 'name': 'some device', 'sys_path': '/sys/something', }), Resource({ 'attr': 'bad value', 'dev_name': 'sda2', 'name': 'some other device', 'sys_path': '/sys/something-else', }) ]) self.assertEqual(len(unit_list), 1) self.assertEqual(unit_list[0].partial_id, 'check-device-sda1')
def test_evaluate_normal(self): # NOTE: the actual expr.resource_name is irrelevant for this test expr = ResourceExpression("obj.a == 2") self.assertTrue(expr.evaluate([Resource({'a': 1}), Resource({'a': 2})])) self.assertTrue(expr.evaluate([Resource({'a': 2}), Resource({'a': 1})])) self.assertFalse( expr.evaluate([Resource({'a': 1}), Resource({'a': 3})]))
def test_instantiate_one(self): template = TemplateUnit({ 'template-resource': 'resource', 'id': 'check-device-{dev_name}', 'summary': 'Test {name} ({sys_path})', 'plugin': 'shell', }) job = template.instantiate_one(Resource({ 'dev_name': 'sda1', 'name': 'some device', 'sys_path': '/sys/something', })) self.assertIsInstance(job, JobDefinition) self.assertEqual(job.partial_id, 'check-device-sda1') self.assertEqual(job.summary, 'Test some device (/sys/something)') self.assertEqual(job.plugin, 'shell')
def run_generator_job(self, checksum, env): """ Run a job with and process the stdout to get a job definition. :param checksum: The checksum of the job to execute :param env: Environment to execute the job in. :returns: A list of job definitions that were processed from the output. :raises LookupError: If the checksum does not match any known job """ job = self.find_job(checksum) cmd = [job.shell, '-c', job.command] output = subprocess.check_output( cmd, universal_newlines=True, env=self.modify_execution_environment(env)) job_list = [] source = JobOutputTextSource(job) try: record_list = load_rfc822_records(output, source=source) except RFC822SyntaxError as exc: logging.error(_("Syntax error in record generated from %s: %s"), job, exc) else: if job.plugin == 'local': for record in record_list: job = JobDefinition.from_rfc822_record(record) job_list.append(job) elif job.plugin == 'resource': resource_list = [] for record in record_list: resource = Resource(record.data) resource_list.append(resource) for plugin in all_providers.get_all_plugins(): for u in plugin.plugin_object.unit_list: if (isinstance(u, TemplateUnit) and u.resource_id == job.id): logging.info(_("Instantiating unit: %s"), u) for new_unit in u.instantiate_all(resource_list): job_list.append(new_unit) return job_list
def test_instantiate_missing_parameter(self): """ Ensure that a MissingParam exeception is raised when attempting to instantiate a template unit that contains a paremeter not present in the associated resource. """ template = TemplateUnit({ 'template-resource': 'resource', 'id': 'check-device-{missing}', 'plugin': 'shell', }) job = template.instantiate_one(Resource({ 'dev_name': 'sda1', 'name': 'some device', 'sys_path': '/sys/something', })) self.assertIsInstance(job, JobDefinition) with self.assertRaises(MissingParam): self.assertEqual(job.partial_id, 'check-device-sda1')
def test_setattr(self): res = Resource() res.attr = 'value' self.assertEqual(res.attr, 'value') res.attr = 'other value' self.assertEqual(res.attr, 'other value')