Merge lp:~zyga/checkbox/no-resource-limits into lp:checkbox

Proposed by Zygmunt Krynicki on 2015-09-24
Status: Merged
Approved by: Zygmunt Krynicki on 2015-09-25
Approved revision: 4030
Merged at revision: 4031
Proposed branch: lp:~zyga/checkbox/no-resource-limits
Merge into: lp:checkbox
Diff against target: 615 lines (+174/-153)
7 files modified
plainbox/docs/changelog.rst (+14/-0)
plainbox/docs/dev/resources.rst (+31/-42)
plainbox/plainbox/impl/commands/inv_special.py (+6/-5)
plainbox/plainbox/impl/ctrl.py (+34/-29)
plainbox/plainbox/impl/resource.py (+65/-46)
plainbox/plainbox/impl/test_resource.py (+22/-22)
plainbox/plainbox/impl/unit/test_job.py (+2/-9)
To merge this branch: bzr merge lp:~zyga/checkbox/no-resource-limits
Reviewer Review Type Date Requested Status
Sylvain Pineau 2015-09-24 Approve on 2015-09-25
Review via email: mp+272319@code.launchpad.net
To post a comment you must log in.
Sylvain Pineau (sylvain-pineau) wrote :

Thanks for the doc update, it really helps understanding what's possible with resources expressions now. I even didn't realized that you could also introduce imported resources, that's even better.

+1

review: Approve
Sylvain Pineau (sylvain-pineau) wrote :

Attempt to merge into lp:checkbox failed due to conflicts:

text conflict in plainbox/docs/changelog.rst

lp:~zyga/checkbox/no-resource-limits updated on 2015-09-25
4030. By Zygmunt Krynicki on 2015-09-25

plainbox: add support for multiple resource requirements

This patch essentially allows a requirement program to use more than one
resource. In practice this unlocks a host of capabilities where we can
freely combine manifests, syfs data, udev data and anything else that
makes sense in a given context and turn that into a resource expression.

The primary consumer of this is the TPM provider that has some
non-trivial logic in requirement programs. This logic allows us to
construct a flat lost of jobs to run and thanks to the more complicated
expressions, get them to effectively implement and if-the-else
navigation among jobs so that regardless of what state the TPM hardware
is at the beginning of the test, we can correctly navigate the user
through the testing process.

Technically the ResourceNodeVisitor now collects a list of names as it
traverses the program AST. The MultipleResourcesReferenced exception is
gone entirely. The ResourceExpression class has an API change, as
.resource_id becomes a .resource_id_list. There is a similar change in
_resource_alias_list but this API is private.

There are surprisingly few users of resource_id. Most of the work was in
the controller module (ctrl.py) where we now look at all the resources
to convert them to dependencies. There are a few odd test cases for job
definition units and one utility script that generates .dot diagrams.

The most important note is, that multiple resources should be
discouraged for resource jobs that produce many resource records. The
complexity of evaluating a resource expression over N resources is the
product of the number of resource records in each resource. It's safe to
say that it works best for resource jobs with just one record.

At a later stage we may implement some simple low-hanging optimizations
where resource computation like res.attr == "value" can be converted
from O(N) to O(1) per evaluation at an O(N) one-time cost.

Signed-off-by: Zygmunt Krynicki <email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'plainbox/docs/changelog.rst'
2--- plainbox/docs/changelog.rst 2015-09-25 07:28:31 +0000
3+++ plainbox/docs/changelog.rst 2015-09-25 08:41:19 +0000
4@@ -23,6 +23,20 @@
5 attachment jobs will automatically pull in their log-generating cousins and
6 will run at the right time no matter what happens.
7
8+* Plainbox now allows more than one resource object to be used in a resource
9+ expression. This can be used to construct resource expressions that combine
10+ facts from multiple sources (e.g. the manifest resource with something else).
11+
12+ As an **important** implementation limitation please remember that the
13+ complexity of such resource programs is proportional to the product of the
14+ number of resource objects associated with each resource in an expression.
15+ In practice it is not advised to use resource objects with more than a few
16+ resource records associated with them. This is just an implementation detail
17+ that can be lifted in subsequent versions.
18+
19+ Examples of the usage of this feature can be found in the TPM (Trusted
20+ Platform Module) provider.
21+
22 .. _version_0_23:
23
24 Plainbox 0.23 (QA Testing)
25
26=== modified file 'plainbox/docs/dev/resources.rst'
27--- plainbox/docs/dev/resources.rst 2014-09-16 12:53:41 +0000
28+++ plainbox/docs/dev/resources.rst 2015-09-25 08:41:19 +0000
29@@ -69,14 +69,12 @@
30
31 Anything else is rejected as an invalid resource expression.
32
33-In addition to that, each resource expression must use exactly one variable,
34+In addition to that, each resource expression must use at least one variable,
35 which must be used like an object with attributes. The name of that variable
36-must correspond to the name of the job that generates resources. Attempts to
37-use more than one variable or to not use any variables are detected early and
38-rejected as invalid resource expressions.
39-
40-The name of the variable determines which resource group to use. It must match
41-the name of the job that generates such resources.
42+must correspond to the name of the job that generates resources. You can use
43+the ``imports`` field (at a job definition level) to rename a resource job to
44+be compatible with the identifier syntax. It can also be used to refer to
45+resources from another namespace.
46
47 In the examples elsewhere in this page the ``package`` resources are generated
48 by the ``package`` job. Plainbox uses this to know which resources to try but
49@@ -112,41 +110,32 @@
50 non-exhaustive, it only contains issues that we came across found not to work
51 in practice.
52
53-Exactly one variable per expression
54-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
55-
56-Each resource expression must refer to exactly one variable. This is a side
57-effect of the way the evaluator works. It basically bind one object (a
58-particular resource) to that variable and evaluates the expression.
59-
60-The expression parser / syntax analyzer identifies expressions with this
61-problem early and rejects them with an appropriate error message. Here are
62-some examples of hypothetical expressions that exhibit this problem.
63-
64-"I want to have mplayer and an audio device so that I can play some sounds"::
65-
66- device.category == "AUDIO" and package.name == "mplayer"
67-
68-To work around this, split the expression to two separate expressions. The
69-evaluator will put an implicit ``and`` between them and it will do exactly what
70-you intended::
71-
72- device.category == "AUDIO"
73- package.name == "mplayer"
74-
75-"I want to always run this test"::
76-
77- True
78-
79-To work around this, simply remove the requirement program entirely!
80-
81-"I want to never run this test"::
82-
83- False
84-
85-To work around this remove this job from the selection. You may also use a
86-special resource that produces one constant value, and check that it is equal
87-to something different.
88+Joins are not optimized
89+^^^^^^^^^^^^^^^^^^^^^^^
90+
91+Starting with plainbox 0.24, a resource expression can use more than one
92+resource object (resource job) at the same time. This allows the use of joins
93+as the whole expression is evaluated over the cartesian product of all the
94+resource records. This operation is not optimized, you can think of it as a
95+JOIN that is performed on a database without any indices.
96+
97+Let's look at a practical example::
98+
99+ package.name == desired_package.name
100+
101+Here, two resource jobs would run. The classic *package* resource (that
102+produces, typically, a great number of resource records, one for each package
103+installed on the system) and a hypothetical *desired_package* resource (for
104+this example let's pretend that it is a simple constant resource that just
105+contains one object). Here, this operation is not any worse than before because
106+``size(desired_package) * size(package)`` is not any larger. If, however,
107+*desired_package* was on the same order as *package* (approximately a thousand
108+resource objects). Then the computational cost of evaluating that expression
109+would be quadratic.
110+
111+In general, the cost, assuming all resources have the same order, is
112+exponential with the number of distinct resource jobs referenced by the
113+expression.
114
115 Exactly one resource bound to a variable at once
116 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
117
118=== modified file 'plainbox/plainbox/impl/commands/inv_special.py'
119--- plainbox/plainbox/impl/commands/inv_special.py 2014-11-18 17:20:58 +0000
120+++ plainbox/plainbox/impl/commands/inv_special.py 2015-09-25 08:41:19 +0000
121@@ -103,9 +103,10 @@
122 prog = job.get_resource_program()
123 if ns.dot_resources and prog is not None:
124 for expression in prog.expression_list:
125- print('\t"{}" [shape=ellipse,color=blue];'.format(
126- expression.resource_id))
127- print('\t"{}" -> "{}" [style=dashed, label="{}"];'.format(
128- job.id, expression.resource_id,
129- expression.text.replace('"', "'")))
130+ for resource_id in expression.resource_id_list:
131+ print('\t"{}" [shape=ellipse,color=blue];'.format(
132+ resource_id))
133+ print('\t"{}" -> "{}" [style=dashed, label="{}"];'.format(
134+ job.id, resource_id,
135+ expression.text.replace('"', "'")))
136 print("}")
137
138=== modified file 'plainbox/plainbox/impl/ctrl.py'
139--- plainbox/plainbox/impl/ctrl.py 2015-09-25 07:28:31 +0000
140+++ plainbox/plainbox/impl/ctrl.py 2015-09-25 08:41:19 +0000
141@@ -161,36 +161,41 @@
142 try:
143 prog.evaluate_or_raise(session_state.resource_map)
144 except ExpressionCannotEvaluateError as exc:
145- # Lookup the related job (the job that provides the
146- # resources needed by the expression that cannot be
147- # evaluated)
148- related_job = session_state.job_state_map[
149- exc.expression.resource_id].job
150- # Add A PENDING_RESOURCE inhibitor as we are unable to
151- # determine if the resource requirement is met or not. This
152- # can happen if the resource job did not ran for any reason
153- # (it can either be prevented from running by normal means
154- # or simply be on the run_list but just was not executed
155- # yet).
156- inhibitor = JobReadinessInhibitor(
157- cause=InhibitionCause.PENDING_RESOURCE,
158- related_job=related_job,
159- related_expression=exc.expression)
160- inhibitors.append(inhibitor)
161+ for resource_id in exc.expression.resource_id_list:
162+ if session_state.job_state_map[resource_id].result.outcome == 'pass':
163+ continue
164+ # Lookup the related job (the job that provides the
165+ # resources needed by the expression that cannot be
166+ # evaluated)
167+ related_job = session_state.job_state_map[resource_id].job
168+ # Add A PENDING_RESOURCE inhibitor as we are unable to
169+ # determine if the resource requirement is met or not. This
170+ # can happen if the resource job did not ran for any reason
171+ # (it can either be prevented from running by normal means
172+ # or simply be on the run_list but just was not executed
173+ # yet).
174+ inhibitor = JobReadinessInhibitor(
175+ cause=InhibitionCause.PENDING_RESOURCE,
176+ related_job=related_job,
177+ related_expression=exc.expression)
178+ inhibitors.append(inhibitor)
179 except ExpressionFailedError as exc:
180- # Lookup the related job (the job that provides the
181- # resources needed by the expression that failed)
182- related_job = session_state.job_state_map[
183- exc.expression.resource_id].job
184- # Add a FAILED_RESOURCE inhibitor as we have all the data
185- # to run the requirement program but it simply returns a
186- # non-True value. This typically indicates a missing
187- # software package or necessary hardware.
188- inhibitor = JobReadinessInhibitor(
189- cause=InhibitionCause.FAILED_RESOURCE,
190- related_job=related_job,
191- related_expression=exc.expression)
192- inhibitors.append(inhibitor)
193+ # When expressions fail then all the associated resources are
194+ # marked as failed since we don't want to get into the analysis
195+ # of logic expressions to know any "better".
196+ for resource_id in exc.expression.resource_id_list:
197+ # Lookup the related job (the job that provides the
198+ # resources needed by the expression that failed)
199+ related_job = session_state.job_state_map[resource_id].job
200+ # Add a FAILED_RESOURCE inhibitor as we have all the data
201+ # to run the requirement program but it simply returns a
202+ # non-True value. This typically indicates a missing
203+ # software package or necessary hardware.
204+ inhibitor = JobReadinessInhibitor(
205+ cause=InhibitionCause.FAILED_RESOURCE,
206+ related_job=related_job,
207+ related_expression=exc.expression)
208+ inhibitors.append(inhibitor)
209 # Check if all job dependencies ran successfully
210 for dep_id in sorted(job.get_direct_dependencies()):
211 dep_job_state = session_state.job_state_map[dep_id]
212
213=== modified file 'plainbox/plainbox/impl/resource.py'
214--- plainbox/plainbox/impl/resource.py 2015-05-18 16:59:31 +0000
215+++ plainbox/plainbox/impl/resource.py 2015-09-25 08:41:19 +0000
216@@ -27,6 +27,7 @@
217 """
218
219 import ast
220+import itertools
221 import logging
222
223 from plainbox.i18n import gettext as _
224@@ -67,9 +68,13 @@
225 enough data to provide rich and meaningful error messages to the operator.
226 """
227
228+ def __init__(self, expression, resource_id):
229+ self.expression = expression
230+ self.resource_id = resource_id
231+
232 def __str__(self):
233 return _("expression {!r} needs unavailable resource {!r}").format(
234- self.expression.text, self.expression.resource_id)
235+ self.expression.text, self.resource_id)
236
237
238 class Resource:
239@@ -220,8 +225,11 @@
240 """
241 A set() of resource ids that are needed to evaluate this program
242 """
243- return set((expression.resource_id
244- for expression in self._expression_list))
245+ ids = set()
246+ for expression in self._expression_list:
247+ for resource_id in expression.resource_id_list:
248+ ids.add(resource_id)
249+ return ids
250
251 def evaluate_or_raise(self, resource_map):
252 """
253@@ -238,12 +246,16 @@
254 """
255 # First check if we have all required resources
256 for expression in self._expression_list:
257- if expression.resource_id not in resource_map:
258- raise ExpressionCannotEvaluateError(expression)
259+ for resource_id in expression.resource_id_list:
260+ if resource_id not in resource_map:
261+ raise ExpressionCannotEvaluateError(
262+ expression, resource_id)
263 # Then evaluate all expressions
264 for expression in self._expression_list:
265- result = expression.evaluate(
266- resource_map[expression.resource_id])
267+ result = expression.evaluate(*[
268+ resource_map[resource_id]
269+ for resource_id in expression.resource_id_list
270+ ])
271 if not result:
272 raise ExpressionFailedError(expression)
273 return True
274@@ -378,16 +390,24 @@
275
276 def __init__(self):
277 """
278- Initialize a ResourceNodeVisitor with empty set of ids_seen
279+ Initialize a ResourceNodeVisitor with empty trace of seen identifiers
280 """
281- self._ids_seen = set()
282+ self._ids_seen_set = set()
283+ self._ids_seen_list = []
284
285 @property
286- def ids_seen(self):
287+ def ids_seen_set(self):
288 """
289 set() of ast.Name().id values seen
290 """
291- return self._ids_seen
292+ return self._ids_seen_set
293+
294+ @property
295+ def ids_seen_list(self):
296+ """
297+ list() of ast.Name().id values seen
298+ """
299+ return self._ids_seen_list
300
301 def visit_Name(self, node):
302 """
303@@ -397,7 +417,9 @@
304 ast.Name(). It records the node identifier and calls _check_node()
305 """
306 self._check_node(node)
307- self._ids_seen.add(node.id)
308+ if node.id not in self._ids_seen_set:
309+ self._ids_seen_set.add(node.id)
310+ self._ids_seen_list.append(node.id)
311
312 def visit_Call(self, node):
313 """
314@@ -470,15 +492,6 @@
315 return _("expression did not reference any resources")
316
317
318-class MultipleResourcesReferenced(ResourceProgramError):
319- """
320- Exception raised when an expression references multiple resources.
321- """
322-
323- def __str__(self):
324- return _("expression referenced multiple resources")
325-
326-
327 class ResourceSyntaxError(ResourceProgramError):
328
329 def __str__(self):
330@@ -502,20 +515,22 @@
331 May raise ResourceProgramError
332 """
333 self._implicit_namespace = implicit_namespace
334- self._resource_alias = self._analyze(text)
335+ self._resource_alias_list = self._analyze(text)
336+ self._resource_id_list = []
337 if imports is None:
338 imports = ()
339 # Respect any import statements.
340 # They always take priority over anything we may know locally
341- for imported_resource_id, imported_alias in imports:
342- if imported_alias == self._resource_alias:
343- self._resource_id = imported_resource_id
344- break
345- else:
346- self._resource_id = self._resource_alias
347+ for resource_alias in self._resource_alias_list:
348+ for imported_resource_id, imported_alias in imports:
349+ if imported_alias == resource_alias:
350+ self._resource_id_list.append(imported_resource_id)
351+ break
352+ else:
353+ self._resource_id_list.append(resource_alias)
354 self._text = text
355 self._lambda = eval("lambda {}: {}".format(
356- self._resource_alias, self._text))
357+ ', '.join(self._resource_alias_list), self._text))
358
359 def __str__(self):
360 return self._text
361@@ -541,7 +556,7 @@
362 return self._text
363
364 @property
365- def resource_id(self):
366+ def resource_id_list(self):
367 """
368 The id of the resource this expression depends on
369
370@@ -549,13 +564,15 @@
371 valid python identifier and it is always (ideally) a fully-qualified
372 job identifier.
373 """
374- if "::" not in self._resource_id and self._implicit_namespace:
375- return "{}::{}".format(self._implicit_namespace, self._resource_id)
376- else:
377- return self._resource_id
378+ return [
379+ "{}::{}".format(self._implicit_namespace, resource_id)
380+ if "::" not in resource_id and self._implicit_namespace
381+ else resource_id
382+ for resource_id in self._resource_id_list
383+ ]
384
385 @property
386- def resource_alias(self):
387+ def resource_alias_list(self):
388 """
389 The alias of the resource object this expression operates on
390
391@@ -563,7 +580,7 @@
392 python identifier. The alias is either the partial identifier of the
393 resource job or an arbitrary identifier, as used by the job definition.
394 """
395- return self._resource_alias
396+ return self._resource_alias_list
397
398 @property
399 def implicit_namespace(self):
400@@ -572,7 +589,7 @@
401 """
402 return self._implicit_namespace
403
404- def evaluate(self, resource_list):
405+ def evaluate(self, *resource_list_list):
406 """
407 Evaluate the expression against a list of resources
408
409@@ -580,13 +597,16 @@
410 id in the expression. The return value is True if any of the attempts
411 return a true value, otherwise the result is False.
412 """
413+ for resource_list in resource_list_list:
414+ for resource in resource_list:
415+ if not isinstance(resource, Resource):
416+ raise TypeError(
417+ "Each resource must be a Resource instance")
418 # Try each resource in sequence.
419- for resource in resource_list:
420- if not isinstance(resource, Resource):
421- raise TypeError("Each resource must be a Resource instance")
422+ for resource_pack in itertools.product(*resource_list_list):
423 # Attempt to evaluate the code with the current resource
424 try:
425- result = self._lambda(resource)
426+ result = self._lambda(*resource_pack)
427 except Exception as exc:
428 # Treat any exception as a non-fatal error
429 #
430@@ -594,7 +614,8 @@
431 # why they happen. We could do deeper validation this way.
432 logger.debug(
433 _("Exception in requirement expression %r (with %s=%r):"
434- " %r"), self._text, self._resource_id, resource, exc)
435+ " %r"),
436+ self._text, self._resource_id_list, resource, exc)
437 continue
438 # Treat any true result as a success
439 if result:
440@@ -622,12 +643,10 @@
441 visitor = ResourceNodeVisitor()
442 visitor.visit(node)
443 # Bail if the expression is not using exactly one resource id
444- if len(visitor.ids_seen) == 0:
445+ if len(visitor.ids_seen_list) == 0:
446 raise NoResourcesReferenced()
447- elif len(visitor.ids_seen) == 1:
448- return list(visitor.ids_seen)[0]
449 else:
450- raise MultipleResourcesReferenced()
451+ return list(visitor.ids_seen_list)
452
453
454 def parse_imports_stmt(imports):
455
456=== modified file 'plainbox/plainbox/impl/test_resource.py'
457--- plainbox/plainbox/impl/test_resource.py 2014-08-08 16:35:49 +0000
458+++ plainbox/plainbox/impl/test_resource.py 2015-09-25 08:41:19 +0000
459@@ -31,7 +31,6 @@
460 from plainbox.impl.resource import ExpressionCannotEvaluateError
461 from plainbox.impl.resource import ExpressionFailedError
462 from plainbox.impl.resource import FakeResource
463-from plainbox.impl.resource import MultipleResourcesReferenced
464 from plainbox.impl.resource import NoResourcesReferenced
465 from plainbox.impl.resource import Resource
466 from plainbox.impl.resource import ResourceExpression
467@@ -59,7 +58,7 @@
468
469 def test_smoke(self):
470 expression = ResourceExpression('resource.attr == "value"')
471- exc = ExpressionCannotEvaluateError(expression)
472+ exc = ExpressionCannotEvaluateError(expression, 'resource')
473 self.assertIs(exc.expression, expression)
474 self.assertEqual(str(exc), (
475 "expression 'resource.attr == \"value\"' needs unavailable"
476@@ -183,11 +182,6 @@
477
478 class ResourceProgramErrorTests(TestCase):
479
480- def test_multiple(self):
481- exc = MultipleResourcesReferenced()
482- self.assertEqual(
483- str(exc), "expression referenced multiple resources")
484-
485 def test_none(self):
486 exc = NoResourcesReferenced()
487 self.assertEqual(
488@@ -209,13 +203,15 @@
489
490 def test_smoke(self):
491 visitor = ResourceNodeVisitor()
492- self.assertEqual(visitor.ids_seen, set())
493+ self.assertEqual(visitor.ids_seen_set, set())
494+ self.assertEqual(visitor.ids_seen_list, [])
495
496 def test_ids_seen(self):
497 visitor = ResourceNodeVisitor()
498 node = ast.parse("package.name == 'fwts' and package.version == '1.2'")
499 visitor.visit(node)
500- self.assertEqual(visitor.ids_seen, {'package'})
501+ self.assertEqual(visitor.ids_seen_set, {'package'})
502+ self.assertEqual(visitor.ids_seen_list, ['package'])
503
504 def test_name_assignment_disallowed(self):
505 visitor = ResourceNodeVisitor()
506@@ -289,45 +285,49 @@
507 text = "package.name == 'fwts'"
508 expr = ResourceExpression(text)
509 self.assertEqual(expr.text, text)
510- self.assertEqual(expr.resource_id, "package")
511+ self.assertEqual(expr.resource_id_list, ["package"])
512 self.assertEqual(expr.implicit_namespace, None)
513
514 def test_namespace_support(self):
515 text = "package.name == 'fwts'"
516 expr = ResourceExpression(text, "2014.com.canonical")
517 self.assertEqual(expr.text, text)
518- self.assertEqual(expr.resource_id, "2014.com.canonical::package")
519+ self.assertEqual(expr.resource_id_list,
520+ ["2014.com.canonical::package"])
521 self.assertEqual(expr.implicit_namespace, "2014.com.canonical")
522
523 def test_imports_support(self):
524 text = "package.name == 'fwts'"
525 expr1 = ResourceExpression(text, "2014.com.example")
526 self.assertEqual(expr1.text, text)
527- self.assertEqual(expr1.resource_id, "2014.com.example::package")
528+ self.assertEqual(expr1.resource_id_list, ["2014.com.example::package"])
529 self.assertEqual(expr1.implicit_namespace, "2014.com.example")
530 expr2 = ResourceExpression(text, "2014.com.example", imports=())
531 self.assertEqual(expr2.text, text)
532- self.assertEqual(expr2.resource_id, "2014.com.example::package")
533+ self.assertEqual(expr2.resource_id_list, ["2014.com.example::package"])
534 self.assertEqual(expr2.implicit_namespace, "2014.com.example")
535 expr3 = ResourceExpression(
536 text, "2014.com.example", imports=[
537 ('2014.com.canonical::package', 'package')])
538 self.assertEqual(expr3.text, text)
539- self.assertEqual(expr3.resource_id, "2014.com.canonical::package")
540+ self.assertEqual(expr3.resource_id_list,
541+ ["2014.com.canonical::package"])
542 self.assertEqual(expr3.implicit_namespace, "2014.com.example")
543
544 def test_smoke_bad(self):
545 self.assertRaises(ResourceSyntaxError, ResourceExpression, "barf'")
546 self.assertRaises(CodeNotAllowed, ResourceExpression, "a = 5")
547 self.assertRaises(NoResourcesReferenced, ResourceExpression, "5 < 10")
548- self.assertRaises(MultipleResourcesReferenced,
549- ResourceExpression, "a.foo == 1 and b.bar == 2")
550+
551+ def test_multiple_resources(self):
552+ expr = ResourceExpression("a.foo == 1 and b.bar == 2")
553+ self.assertEqual(expr.resource_id_list, ["a", "b"])
554
555 def test_evaluate_no_namespaces(self):
556 self.assertFalse(ResourceExpression("whatever").evaluate([]))
557
558 def test_evaluate_normal(self):
559- # NOTE: the actual expr.resource_id is irrelevant for this test
560+ # NOTE: the actual expr.resource_id_list is irrelevant for this test
561 expr = ResourceExpression("obj.a == 2")
562 self.assertTrue(
563 expr.evaluate([
564@@ -340,7 +340,7 @@
565 Resource({'a': 1}), Resource({'a': 3})]))
566
567 def test_evaluate_exception(self):
568- # NOTE: the actual expr.resource_id is irrelevant for this test
569+ # NOTE: the actual expr.resource_id_list is irrelevant for this test
570 expr = ResourceExpression("obj.a == 2")
571 self.assertFalse(expr.evaluate([Resource()]))
572
573@@ -362,12 +362,12 @@
574 self.assertEqual(len(self.prog.expression_list), 2)
575 self.assertEqual(self.prog.expression_list[0].text,
576 "package.name == 'fwts'")
577- self.assertEqual(self.prog.expression_list[0].resource_id,
578- "package")
579+ self.assertEqual(self.prog.expression_list[0].resource_id_list,
580+ ["package"])
581 self.assertEqual(self.prog.expression_list[1].text,
582 "platform.arch in ('i386', 'amd64')")
583- self.assertEqual(self.prog.expression_list[1].resource_id,
584- "platform")
585+ self.assertEqual(self.prog.expression_list[1].resource_id_list,
586+ ["platform"])
587
588 def test_required_resources(self):
589 self.assertEqual(self.prog.required_resources,
590
591=== modified file 'plainbox/plainbox/impl/unit/test_job.py'
592--- plainbox/plainbox/impl/unit/test_job.py 2015-09-25 08:10:57 +0000
593+++ plainbox/plainbox/impl/unit/test_job.py 2015-09-25 08:41:19 +0000
594@@ -1110,13 +1110,6 @@
595 observed = job.get_resource_dependencies()
596 self.assertEqual(expected, observed)
597
598- def test_resource_parsing_broken(self):
599- job = JobDefinition({
600- 'id': 'id',
601- 'plugin': 'plugin',
602- 'requires': "foo.bar == bar"})
603- self.assertRaises(Exception, job.get_resource_dependencies)
604-
605 def test_checksum_smoke(self):
606 job1 = JobDefinition({
607 'id': 'id',
608@@ -1410,5 +1403,5 @@
609 "manifest.has_thunderbolt == 'True'\n"),
610 }, provider=provider)
611 prog = job.get_resource_program()
612- self.assertEqual(prog.expression_list[-1].resource_id,
613- '2013.com.canonical.plainbox::manifest')
614+ self.assertEqual(prog.expression_list[-1].resource_id_list,
615+ ['2013.com.canonical.plainbox::manifest'])

Subscribers

People subscribed via source and target branches