Merge lp:~sylvain-pineau/checkbox/nested_testplans into lp:checkbox

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 4401
Merged at revision: 4417
Proposed branch: lp:~sylvain-pineau/checkbox/nested_testplans
Merge into: lp:checkbox
Diff against target: 487 lines (+254/-65)
5 files modified
plainbox/plainbox/impl/commands/inv_special.py (+5/-1)
plainbox/plainbox/impl/session/assistant.py (+3/-5)
plainbox/plainbox/impl/session/manager.py (+1/-1)
plainbox/plainbox/impl/unit/test_testplan.py (+125/-1)
plainbox/plainbox/impl/unit/testplan.py (+120/-57)
To merge this branch: bzr merge lp:~sylvain-pineau/checkbox/nested_testplans
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Paul Larson Approve
Review via email: mp+296999@code.launchpad.net

Description of the change

The nested test plans feature is ready for review.
Instead of a CEP, I'll work on updating the documentation in a separate MR

To use and test this new field in your testplans just add:

nested_part:
    testplan_id # if it belongs to the same namespace
    2013.foo.bar::second_testplan_id # to include jobs from another project namespace

Nota: jobs will follow the nested parts ordering but all "include:" jobs of the "master" testplan if this list is non empty will come first.

Tested with both checkbox-cli and checkbox-converged, additional tests ( the ones coming from the nested parts) are properly included.

To post a comment you must log in.
Revision history for this message
Paul Larson (pwlars) wrote :

Looks good, and unit tests pass. +1

review: Approve
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Let's land it, thanks for the review.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'plainbox/plainbox/impl/commands/inv_special.py'
2--- plainbox/plainbox/impl/commands/inv_special.py 2015-09-25 09:13:53 +0000
3+++ plainbox/plainbox/impl/commands/inv_special.py 2016-06-09 20:31:24 +0000
4@@ -57,7 +57,11 @@
5 # specified just operate on the whole set. The ns.special check
6 # prevents people starting plainbox from accidentally running _all_
7 # jobs without prompting.
8- if ns.special is not None and not ns.include_pattern_list:
9+ if (
10+ ns.special is not None and
11+ not ns.include_pattern_list and
12+ not ns.test_plan
13+ ):
14 matching_job_list = job_list
15 return matching_job_list
16
17
18=== modified file 'plainbox/plainbox/impl/session/assistant.py'
19--- plainbox/plainbox/impl/session/assistant.py 2016-05-24 19:38:24 +0000
20+++ plainbox/plainbox/impl/session/assistant.py 2016-06-09 20:31:24 +0000
21@@ -1059,11 +1059,9 @@
22 """
23 UsageExpectation.of(self).enforce()
24 test_plan = self._manager.test_plans[0]
25- potential_job_list = select_jobs(
26- self._context.state.job_list,
27- [test_plan.get_qualifier(), test_plan.get_mandatory_qualifier()])
28 return list(set(
29- test_plan.get_effective_category_map(potential_job_list).values()))
30+ test_plan.get_effective_category_map(
31+ self._context.state.run_list).values()))
32
33 @raises(UnexpectedMethodCall)
34 def get_mandatory_jobs(self) -> 'Iterable[str]':
35@@ -1432,7 +1430,7 @@
36 UsageExpectation.of(self).enforce()
37 exporter = self._manager.create_exporter(exporter_id, option_list,
38 strict=False)
39-
40+
41 # LP:1585326 maintain isoformat but removing ':' chars that cause
42 # issues when copying files.
43 isoformat = "%Y-%m-%dT%H.%M.%S.%f"
44
45=== modified file 'plainbox/plainbox/impl/session/manager.py'
46--- plainbox/plainbox/impl/session/manager.py 2016-04-29 08:36:20 +0000
47+++ plainbox/plainbox/impl/session/manager.py 2016-06-09 20:31:24 +0000
48@@ -550,8 +550,8 @@
49 repo = SessionStorageRepository(tmp)
50 if provider_list is None:
51 provider_list = get_providers()
52+ try:
53 manager = cls.create(repo=repo)
54- try:
55 manager.add_local_device_context()
56 device_context = manager.default_device_context
57 for provider in provider_list:
58
59=== modified file 'plainbox/plainbox/impl/unit/test_testplan.py'
60--- plainbox/plainbox/impl/unit/test_testplan.py 2015-09-25 06:48:54 +0000
61+++ plainbox/plainbox/impl/unit/test_testplan.py 2016-06-09 20:31:24 +0000
62@@ -33,6 +33,7 @@
63 from plainbox.impl.secure.qualifiers import OperatorMatcher
64 from plainbox.impl.secure.qualifiers import PatternMatcher
65 from plainbox.impl.unit.testplan import TestPlanUnit
66+from plainbox.impl.unit.testplan import TestPlanUnitSupport
67 from plainbox.vendor import mock
68
69
70@@ -133,7 +134,7 @@
71
72 def test_category_override__normal(self):
73 unit = TestPlanUnit({
74- 'category-overrides': 'value',
75+ 'category_overrides': 'value',
76 }, provider=self.provider)
77 self.assertEqual(unit.category_overrides, 'value')
78
79@@ -385,3 +386,126 @@
80 }, provider=self.provider)
81 self.assertEqual(unit.get_bootstrap_job_ids(),
82 set(['ns::Foo', 'ns::Bar']))
83+
84+
85+class TestNestedTestPlan(TestCase):
86+
87+ def setUp(self):
88+ self.provider1 = mock.Mock(name='provider1', spec_set=IProvider1)
89+ self.provider1.namespace = 'ns1'
90+ self.provider2 = mock.Mock(name='provider2', spec_set=IProvider1)
91+ self.provider2.namespace = 'ns2'
92+ self.tp1 = TestPlanUnit({
93+ 'id': 'tp1',
94+ 'unit': 'test-plan',
95+ 'name': 'An example test plan 1',
96+ 'include': 'Foo',
97+ 'nested_part': 'tp2'
98+ }, provider=self.provider1)
99+ self.tp2 = TestPlanUnit({
100+ 'id': 'tp2',
101+ 'unit': 'test-plan',
102+ 'name': 'An example test plan 2',
103+ 'include': 'Bar',
104+ 'mandatory_include': 'Baz',
105+ 'bootstrap_include': 'Qux'
106+ }, provider=self.provider1)
107+ self.tp3 = TestPlanUnit({
108+ 'id': 'tp3',
109+ 'unit': 'test-plan',
110+ 'name': 'An example test plan 3',
111+ 'include': '# nothing\n',
112+ 'nested_part': 'tp2',
113+ 'certification_status_overrides': 'apply blocker to Bar'
114+ }, provider=self.provider1)
115+ self.tp4 = TestPlanUnit({
116+ 'id': 'tp4',
117+ 'unit': 'test-plan',
118+ 'name': 'An example test plan 4',
119+ 'include': '# nothing\n',
120+ 'nested_part': (
121+ 'tp2\n'
122+ 'tp5\n'
123+ )
124+ }, provider=self.provider1)
125+ self.tp5 = TestPlanUnit({
126+ 'id': 'tp5',
127+ 'unit': 'test-plan',
128+ 'name': 'An example test plan 5',
129+ 'include': 'Baz2',
130+ }, provider=self.provider1)
131+ self.tp6 = TestPlanUnit({
132+ 'id': 'tp6',
133+ 'unit': 'test-plan',
134+ 'name': 'An example test plan 6',
135+ 'include': 'Foo',
136+ 'nested_part': 'ns2::tp7'
137+ }, provider=self.provider1)
138+ self.tp7 = TestPlanUnit({
139+ 'id': 'tp7',
140+ 'unit': 'test-plan',
141+ 'name': 'An example test plan 7',
142+ 'include': 'Bar'
143+ }, provider=self.provider2)
144+ self.provider1.unit_list = []
145+ self.provider2.unit_list = [self.tp7]
146+ self.tp7.provider_list = [self.provider1, self.provider2]
147+ for i in range(1, 7):
148+ tp = getattr(self, 'tp{}'.format(i))
149+ tp.provider_list = [self.provider1, self.provider2]
150+ self.provider1.unit_list.append(tp)
151+
152+ def test_nested_tesplan__qualifiers(self):
153+ qual_list = self.tp1.get_qualifier().get_primitive_qualifiers()
154+ mandatory_qual_list = \
155+ self.tp1.get_mandatory_qualifier().get_primitive_qualifiers()
156+ bootstrap_qual_list = \
157+ self.tp1.get_bootstrap_qualifier().get_primitive_qualifiers()
158+ self.assertEqual(qual_list[0].field, 'id')
159+ self.assertIsInstance(qual_list[0].matcher, OperatorMatcher)
160+ self.assertEqual(qual_list[0].matcher.value, 'ns1::Foo')
161+ self.assertEqual(qual_list[0].inclusive, True)
162+ self.assertEqual(qual_list[1].field, 'id')
163+ self.assertIsInstance(qual_list[1].matcher, OperatorMatcher)
164+ self.assertEqual(qual_list[1].matcher.value, 'ns1::Qux')
165+ self.assertEqual(qual_list[1].inclusive, False)
166+ self.assertEqual(qual_list[2].field, 'id')
167+ self.assertIsInstance(qual_list[2].matcher, OperatorMatcher)
168+ self.assertEqual(qual_list[2].matcher.value, 'ns1::Bar')
169+ self.assertEqual(qual_list[2].inclusive, True)
170+ self.assertEqual(mandatory_qual_list[0].field, 'id')
171+ self.assertIsInstance(mandatory_qual_list[0].matcher, OperatorMatcher)
172+ self.assertEqual(mandatory_qual_list[0].matcher.value, 'ns1::Baz')
173+ self.assertEqual(mandatory_qual_list[0].inclusive, True)
174+ self.assertEqual(bootstrap_qual_list[0].field, 'id')
175+ self.assertIsInstance(bootstrap_qual_list[0].matcher, OperatorMatcher)
176+ self.assertEqual(bootstrap_qual_list[0].matcher.value, 'ns1::Qux')
177+ self.assertEqual(bootstrap_qual_list[0].inclusive, True)
178+
179+ def test_nested_tesplan__certification_status_override(self):
180+ support = TestPlanUnitSupport(self.tp3)
181+ self.assertEqual(
182+ support.override_list,
183+ [('^ns1::Bar$', [('certification_status', 'blocker')])])
184+
185+ def test_nested_tesplan__multiple_parts(self):
186+ qual_list = self.tp4.get_qualifier().get_primitive_qualifiers()
187+ self.assertEqual(qual_list[1].field, 'id')
188+ self.assertIsInstance(qual_list[1].matcher, OperatorMatcher)
189+ self.assertEqual(qual_list[1].matcher.value, 'ns1::Bar')
190+ self.assertEqual(qual_list[1].inclusive, True)
191+ self.assertEqual(qual_list[3].field, 'id')
192+ self.assertIsInstance(qual_list[3].matcher, OperatorMatcher)
193+ self.assertEqual(qual_list[3].matcher.value, 'ns1::Baz2')
194+ self.assertEqual(qual_list[3].inclusive, True)
195+
196+ def test_nested_tesplan__multiple_namespaces(self):
197+ qual_list = self.tp6.get_qualifier().get_primitive_qualifiers()
198+ self.assertEqual(qual_list[0].field, 'id')
199+ self.assertIsInstance(qual_list[0].matcher, OperatorMatcher)
200+ self.assertEqual(qual_list[0].matcher.value, 'ns1::Foo')
201+ self.assertEqual(qual_list[0].inclusive, True)
202+ self.assertEqual(qual_list[1].field, 'id')
203+ self.assertIsInstance(qual_list[1].matcher, OperatorMatcher)
204+ self.assertEqual(qual_list[1].matcher.value, 'ns2::Bar')
205+ self.assertEqual(qual_list[1].inclusive, True)
206
207=== modified file 'plainbox/plainbox/impl/unit/testplan.py'
208--- plainbox/plainbox/impl/unit/testplan.py 2016-05-20 13:54:59 +0000
209+++ plainbox/plainbox/impl/unit/testplan.py 2016-06-09 20:31:24 +0000
210@@ -24,6 +24,7 @@
211 import logging
212 import operator
213 import re
214+from functools import lru_cache
215
216 from plainbox.i18n import gettext as _
217 from plainbox.impl.secure.qualifiers import CompositeQualifier
218@@ -237,16 +238,33 @@
219 return self.get_record_value('exclude')
220
221 @property
222+ def nested_part(self):
223+ return self.get_record_value('nested_part')
224+
225+ @property
226 def icon(self):
227 return self.get_record_value('icon')
228
229 @property
230 def category_overrides(self):
231- return self.get_record_value('category-overrides')
232+ return self.get_record_value('category_overrides')
233
234 @property
235 def certification_status_overrides(self):
236- return self.get_record_value('certification-status-overrides')
237+ return self.get_record_value('certification_status_overrides')
238+
239+ @property
240+ def provider_list(self):
241+ """
242+ List of provider to used when calling get_throwaway_manager().
243+ Meant to be used by unit tests only.
244+ """
245+ if hasattr(self, "_provider_list"):
246+ return self._provider_list
247+
248+ @provider_list.setter
249+ def provider_list(self, value):
250+ self._provider_list = value
251
252 @property
253 def estimated_duration(self):
254@@ -299,21 +317,51 @@
255 def get_bootstrap_job_ids(self):
256 """Compute and return a set of job ids from bootstrap_include field."""
257 job_ids = set()
258- if self.bootstrap_include is None:
259- return job_ids
260-
261- class V(Visitor):
262-
263- def visit_Text_node(visitor, node: Text):
264- job_ids.add(self.qualify_id(node.text))
265-
266- def visit_Error_node(visitor, node: Error):
267- logger.warning(_(
268- "unable to parse bootstrap_include: %s"), node.msg)
269-
270- V().visit(WordList.parse(self.bootstrap_include))
271+ if self.bootstrap_include is not None:
272+
273+ class V(Visitor):
274+
275+ def visit_Text_node(visitor, node: Text):
276+ job_ids.add(self.qualify_id(node.text))
277+
278+ def visit_Error_node(visitor, node: Error):
279+ logger.warning(_(
280+ "unable to parse bootstrap_include: %s"), node.msg)
281+
282+ V().visit(WordList.parse(self.bootstrap_include))
283+ for tp_unit in self.get_nested_part():
284+ job_ids |= tp_unit.get_bootstrap_job_ids()
285 return job_ids
286
287+ @lru_cache(maxsize=None)
288+ def get_nested_part(self):
289+ """Compute and return a set of test plan ids from nested_part field."""
290+ nested_parts = []
291+ if self.nested_part is not None:
292+ from plainbox.impl.session import SessionManager
293+ with SessionManager.get_throwaway_manager(self.provider_list) as m:
294+ context = m.default_device_context
295+ testplan_ids = []
296+
297+ class V(Visitor):
298+
299+ def visit_Text_node(visitor, node: Text):
300+ testplan_ids.append(self.qualify_id(node.text))
301+
302+ def visit_Error_node(visitor, node: Error):
303+ logger.warning(_(
304+ "unable to parse nested_part: %s"), node.msg)
305+
306+ V().visit(WordList.parse(self.nested_part))
307+ for tp_id in testplan_ids:
308+ try:
309+ nested_parts.append(context.get_unit(tp_id, 'test plan'))
310+ except KeyError:
311+ logger.warning(_(
312+ "unable to find nested part: %s"), tp_id)
313+ return nested_parts
314+
315+ @lru_cache(maxsize=None)
316 def get_qualifier(self):
317 """
318 Convert this test plan to an equivalent qualifier for job selection
319@@ -326,8 +374,11 @@
320 qual_list.extend(self._gen_qualifiers('include', self.include, True))
321 qual_list.extend(self._gen_qualifiers('exclude', self.exclude, False))
322 qual_list.extend([self.get_bootstrap_qualifier(excluding=True)])
323+ for tp_unit in self.get_nested_part():
324+ qual_list.extend([tp_unit.get_qualifier()])
325 return CompositeQualifier(qual_list)
326
327+ @lru_cache(maxsize=None)
328 def get_mandatory_qualifier(self):
329 """
330 Convert this test plan to an equivalent qualifier for job selection
331@@ -339,20 +390,24 @@
332 qual_list = []
333 qual_list.extend(
334 self._gen_qualifiers('include', self.mandatory_include, True))
335+ for tp_unit in self.get_nested_part():
336+ qual_list.extend([tp_unit.get_mandatory_qualifier()])
337 return CompositeQualifier(qual_list)
338
339+ @lru_cache(maxsize=None)
340 def get_bootstrap_qualifier(self, excluding=False):
341 """
342 Convert this test plan to an equivalent qualifier for job selection
343 """
344 qual_list = []
345- if self.bootstrap_include is None:
346- return CompositeQualifier(qual_list)
347- field_origin = self.origin.just_line().with_offset(
348- self.field_offset_map['bootstrap_include'])
349- qual_list = [FieldQualifier(
350- 'id', OperatorMatcher(operator.eq, target_id), field_origin,
351- not excluding) for target_id in self.get_bootstrap_job_ids()]
352+ if self.bootstrap_include is not None:
353+ field_origin = self.origin.just_line().with_offset(
354+ self.field_offset_map['bootstrap_include'])
355+ qual_list = [FieldQualifier(
356+ 'id', OperatorMatcher(operator.eq, target_id), field_origin,
357+ not excluding) for target_id in self.get_bootstrap_job_ids()]
358+ for tp_unit in self.get_nested_part():
359+ qual_list.extend([tp_unit.get_bootstrap_qualifier(excluding)])
360 return CompositeQualifier(qual_list)
361
362 def _gen_qualifiers(self, field_name, field_value, inclusive):
363@@ -547,6 +602,7 @@
364 mandatory_include = 'mandatory_include'
365 bootstrap_include = 'bootstrap_include'
366 exclude = 'exclude'
367+ nested_part = 'nested_part'
368 estimated_duration = 'estimated_duration'
369 icon = 'icon'
370 category_overrides = 'category-overrides'
371@@ -602,6 +658,9 @@
372 fields.exclude: [
373 NonEmptyPatternIntersectionValidator,
374 ],
375+ fields.nested_part: [
376+ NonEmptyPatternIntersectionValidator,
377+ ],
378 fields.estimated_duration: [
379 UntranslatableFieldValidator,
380 TemplateInvariantFieldValidator,
381@@ -649,17 +708,17 @@
382 Some examples of how that works, given this test plan:
383 >>> testplan = TestPlanUnit({
384 ... 'include': '''
385- ... job-a certification-status=blocker, category-id=example
386- ... job-b certification-status=non-blocker
387+ ... job-a certification_status=blocker, category-id=example
388+ ... job-b certification_status=non-blocker
389 ... job-c
390 ... ''',
391 ... 'exclude': '''
392 ... job-[x-z]
393 ... ''',
394- ... 'category-overrides': '''
395+ ... 'category_overrides': '''
396 ... apply other-example to job-[bc]
397 ... ''',
398- ... 'certification-status-overrides': '''
399+ ... 'certification_status_overrides': '''
400 ... apply not-part-of-certification to job-c
401 ... ''',
402 ... })
403@@ -810,6 +869,8 @@
404 override_list.append((pattern, 'category_id', category_id))
405
406 V().visit(OverrideFieldList.parse(testplan.category_overrides))
407+ for tp_unit in testplan.get_nested_part():
408+ override_list.extend(self._get_category_overrides(tp_unit))
409 return override_list
410
411 def _get_blocker_status_overrides(
412@@ -824,20 +885,21 @@
413 is the overridden value.
414 """
415 override_list = []
416- if testplan.certification_status_overrides is None:
417- return override_list
418-
419- class V(Visitor):
420-
421- def visit_FieldOverride_node(self, node: FieldOverride):
422- blocker_status = node.value.text
423- pattern = r"^{}$".format(
424- testplan.qualify_id(node.pattern.text))
425- override_list.append(
426- (pattern, 'certification_status', blocker_status))
427-
428- V().visit(OverrideFieldList.parse(
429- testplan.certification_status_overrides))
430+ if testplan.certification_status_overrides is not None:
431+
432+ class V(Visitor):
433+
434+ def visit_FieldOverride_node(self, node: FieldOverride):
435+ blocker_status = node.value.text
436+ pattern = r"^{}$".format(
437+ testplan.qualify_id(node.pattern.text))
438+ override_list.append(
439+ (pattern, 'certification_status', blocker_status))
440+
441+ V().visit(OverrideFieldList.parse(
442+ testplan.certification_status_overrides))
443+ for tp_unit in testplan.get_nested_part():
444+ override_list.extend(self._get_blocker_status_overrides(tp_unit))
445 return override_list
446
447 def _get_inline_overrides(
448@@ -850,21 +912,22 @@
449 subsequently packed into a tuple ``(pattern, field_value_list)``.
450 """
451 override_list = []
452- if testplan.include is None:
453- return override_list
454-
455- class V(Visitor):
456-
457- def visit_IncludeStmt_node(self, node: IncludeStmt):
458- if not node.overrides:
459- return
460- pattern = r"^{}$".format(
461- testplan.qualify_id(node.pattern.text))
462- field_value_list = [
463- (override_exp.field.text.replace('-', '_'),
464- override_exp.value.text)
465- for override_exp in node.overrides]
466- override_list.append((pattern, field_value_list))
467-
468- V().visit(IncludeStmtList.parse(testplan.include))
469+ if testplan.include is not None:
470+
471+ class V(Visitor):
472+
473+ def visit_IncludeStmt_node(self, node: IncludeStmt):
474+ if not node.overrides:
475+ return
476+ pattern = r"^{}$".format(
477+ testplan.qualify_id(node.pattern.text))
478+ field_value_list = [
479+ (override_exp.field.text.replace('-', '_'),
480+ override_exp.value.text)
481+ for override_exp in node.overrides]
482+ override_list.append((pattern, field_value_list))
483+
484+ V().visit(IncludeStmtList.parse(testplan.include))
485+ for tp_unit in testplan.get_nested_part():
486+ override_list.extend(self._get_inline_overrides(tp_unit))
487 return override_list

Subscribers

People subscribed via source and target branches