Merge ~sylvain-pineau/checkbox-ng:tp-export into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- tp-export
- Merge into master
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | d138689e1755668516c083fc6ccddeeb731e9248 |
Merged at revision: | 4e1a267400404663bceb5f83cd0a94835a050dcb |
Proposed branch: | ~sylvain-pineau/checkbox-ng:tp-export |
Merge into: | checkbox-ng:master |
Diff against target: |
587 lines (+246/-29) 10 files modified
checkbox_ng/launcher/checkbox_cli.py (+3/-1) checkbox_ng/launcher/subcommands.py (+30/-0) docs/using.rst (+27/-0) plainbox/impl/ctrl.py (+14/-6) plainbox/impl/exporter/xlsx.py (+102/-13) plainbox/impl/providers/exporters/units/exporter.pxu (+7/-0) plainbox/impl/runner.py (+32/-0) plainbox/impl/session/assistant.py (+13/-6) plainbox/impl/session/state.py (+4/-2) plainbox/impl/unit/template.py (+14/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Maciej Kisielewski (community) | Approve | ||
Sylvain Pineau (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
Description of the change
New command line subcommand to generate test case guide based on a given test plan unit.
Example:
$ checkbox-cli tp-export com.canonical.
Or to export a PDF:
$ checkbox-cli tp-export com.canonical.
Sample output: https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
Corrected version this time using a special FakeJobRunner class. the tp name is no longer passed trough all layers, instead i'm passing a boolean flag, "fake_resources".
Tested using:
$ checkbox-cli tp-export com.canonical.
$ checkbox-cli tp-export com.canonical.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Maciej Kisielewski (kissiel) wrote : | # |
I like it much more! I also sense that we may use this approach in the future for something else :)
Thank you and +1.
One thing to consider below.
Preview Diff
1 | diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py |
2 | index 59202e7..8457a3f 100644 |
3 | --- a/checkbox_ng/launcher/checkbox_cli.py |
4 | +++ b/checkbox_ng/launcher/checkbox_cli.py |
5 | @@ -40,7 +40,8 @@ from plainbox.impl.launcher import DefaultLauncherDefinition |
6 | from plainbox.impl.launcher import LauncherDefinition |
7 | |
8 | from checkbox_ng.launcher.subcommands import ( |
9 | - Launcher, List, Run, StartProvider, Submit, ListBootstrapped |
10 | + Launcher, List, Run, StartProvider, Submit, ListBootstrapped, |
11 | + TestPlanExport |
12 | ) |
13 | from checkbox_ng.launcher.check_config import CheckConfig |
14 | from checkbox_ng.launcher.remote import RemoteSlave, RemoteMaster |
15 | @@ -161,6 +162,7 @@ class CheckboxCommand(CanonicalCommand): |
16 | ('startprovider', StartProvider), |
17 | ('submit', Submit), |
18 | ('list-bootstrapped', ListBootstrapped), |
19 | + ('tp-export', TestPlanExport), |
20 | ('slave', RemoteSlave), |
21 | ('master', RemoteMaster), |
22 | ) |
23 | diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py |
24 | index cb31211..768c682 100644 |
25 | --- a/checkbox_ng/launcher/subcommands.py |
26 | +++ b/checkbox_ng/launcher/subcommands.py |
27 | @@ -973,6 +973,36 @@ class ListBootstrapped(Command): |
28 | print(job_id) |
29 | |
30 | |
31 | +class TestPlanExport(Command): |
32 | + name = 'tp-export' |
33 | + |
34 | + @property |
35 | + def sa(self): |
36 | + return self.ctx.sa |
37 | + |
38 | + def register_arguments(self, parser): |
39 | + parser.add_argument( |
40 | + 'TEST_PLAN', |
41 | + help=_("test-plan id to bootstrap")) |
42 | + |
43 | + def invoked(self, ctx): |
44 | + self.ctx = ctx |
45 | + try_selecting_providers(self.sa, '*') |
46 | + from plainbox.impl.runner import FakeJobRunner |
47 | + self.sa.start_new_session('tp-export-ephemeral', FakeJobRunner) |
48 | + self.sa._context.state._fake_resources = True |
49 | + tps = self.sa.get_test_plans() |
50 | + if ctx.args.TEST_PLAN not in tps: |
51 | + raise SystemExit('Test plan not found') |
52 | + self.sa.select_test_plan(ctx.args.TEST_PLAN) |
53 | + self.sa.bootstrap() |
54 | + path = self.sa.export_to_file( |
55 | + 'com.canonical.plainbox::tp-export', [], |
56 | + self.sa._manager.storage.location, |
57 | + self.sa._manager.test_plans[0].name) |
58 | + print(path) |
59 | + |
60 | + |
61 | def try_selecting_providers(sa, *args, **kwargs): |
62 | """ |
63 | Try selecting proivders via SessionAssistant. |
64 | diff --git a/docs/using.rst b/docs/using.rst |
65 | index cee2785..0d9bccd 100644 |
66 | --- a/docs/using.rst |
67 | +++ b/docs/using.rst |
68 | @@ -131,6 +131,33 @@ Similarly to the ``checkbox-cli list all-jobs`` command, the output of |
69 | See ``checkbox-cli list`` :ref:`output-formatting` section for more information. |
70 | |
71 | |
72 | +checkbox-cli tp-export |
73 | +`````````````````````` |
74 | + |
75 | +``tp-export`` exports a test plan as a spreadsheet document. Tests are grouped |
76 | +by categories and ordered alphabetically with the full description (or the job |
77 | +summary if there's no description). I addition to the description, the |
78 | +certification status (blocker/non-blocker) is exported. |
79 | + |
80 | +The session is similar to ``list-bootstrapped`` but all resource jobs are |
81 | +returning fake objects and template-filters are disabled to ensure |
82 | +instantiation of template units. By default only one resource object is |
83 | +returned. The only exception is the graphics_card resource where two objects are |
84 | +used to simulate hybrid graphics. |
85 | + |
86 | +The command prints the full path to the document on exit/success. |
87 | + |
88 | +Example:: |
89 | + |
90 | + $ checkbox-cli tp-export com.canonical.certification::client-cert-18-04 |
91 | + |
92 | +It can be used to automatically generate a test case guide using a pdf converter: |
93 | + |
94 | +Example:: |
95 | + |
96 | + $ checkbox-cli tp-export com.canonical.certification::client-cert-18-04 | xargs -d '\n' libreoffice --headless --invisible --convert-to pdf |
97 | + |
98 | + |
99 | checkbox-cli launcher |
100 | ````````````````````` |
101 | |
102 | diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py |
103 | index ef1441c..471dd5f 100644 |
104 | --- a/plainbox/impl/ctrl.py |
105 | +++ b/plainbox/impl/ctrl.py |
106 | @@ -224,7 +224,8 @@ class CheckBoxSessionStateController(ISessionStateController): |
107 | inhibitors.append(inhibitor) |
108 | return inhibitors |
109 | |
110 | - def observe_result(self, session_state, job, result): |
111 | + def observe_result(self, session_state, job, result, |
112 | + fake_resources=False): |
113 | """ |
114 | Notice the specified test result and update readiness state. |
115 | |
116 | @@ -234,6 +235,9 @@ class CheckBoxSessionStateController(ISessionStateController): |
117 | A JobDefinition object |
118 | :param result: |
119 | A IJobResult object |
120 | + :param fake_resources: |
121 | + An optional parameter to trigger test plan export execution mode |
122 | + using fake resourceobjects |
123 | |
124 | This function updates the internal result collection with the data from |
125 | the specified test result. Results can safely override older results. |
126 | @@ -256,15 +260,18 @@ class CheckBoxSessionStateController(ISessionStateController): |
127 | session_state.on_job_result_changed(job, result) |
128 | # Treat some jobs specially and interpret their output |
129 | if job.plugin == "resource": |
130 | - self._process_resource_result(session_state, job, result) |
131 | + self._process_resource_result( |
132 | + session_state, job, result, fake_resources) |
133 | |
134 | - def _process_resource_result(self, session_state, job, result): |
135 | + def _process_resource_result(self, session_state, job, result, |
136 | + fake_resources=False): |
137 | """ |
138 | Analyze a result of a CheckBox "resource" job and generate |
139 | or replace resource records. |
140 | """ |
141 | self._parse_and_store_resource(session_state, job, result) |
142 | - self._instantiate_templates(session_state, job, result) |
143 | + self._instantiate_templates( |
144 | + session_state, job, result, fake_resources) |
145 | |
146 | def _parse_and_store_resource(self, session_state, job, result): |
147 | # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928 |
148 | @@ -289,7 +296,8 @@ class CheckBoxSessionStateController(ISessionStateController): |
149 | # Replace any old resources with the new resource list |
150 | session_state.set_resource_list(job.id, new_resource_list) |
151 | |
152 | - def _instantiate_templates(self, session_state, job, result): |
153 | + def _instantiate_templates(self, session_state, job, result, |
154 | + fake_resources=False): |
155 | # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928 |
156 | # If we are resuming from a session that had a resource job that |
157 | # never ran, we will see an empty MemoryJobResult object. |
158 | @@ -302,7 +310,7 @@ class CheckBoxSessionStateController(ISessionStateController): |
159 | if isinstance(unit, TemplateUnit) and unit.resource_id == job.id: |
160 | logger.info(_("Instantiating unit: %s"), unit) |
161 | for new_unit in unit.instantiate_all( |
162 | - session_state.resource_map[job.id]): |
163 | + session_state.resource_map[job.id], fake_resources): |
164 | try: |
165 | check_result = new_unit.check() |
166 | except MissingParam as m: |
167 | diff --git a/plainbox/impl/exporter/xlsx.py b/plainbox/impl/exporter/xlsx.py |
168 | index 360a368..f582339 100644 |
169 | --- a/plainbox/impl/exporter/xlsx.py |
170 | +++ b/plainbox/impl/exporter/xlsx.py |
171 | @@ -64,12 +64,14 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
172 | OPTION_WITH_SUMMARY = 'with-summary' |
173 | OPTION_WITH_DESCRIPTION = 'with-job-description' |
174 | OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments' |
175 | + OPTION_TEST_PLAN_EXPORT = 'tp-export' |
176 | |
177 | SUPPORTED_OPTION_LIST = ( |
178 | OPTION_WITH_SYSTEM_INFO, |
179 | OPTION_WITH_SUMMARY, |
180 | OPTION_WITH_DESCRIPTION, |
181 | OPTION_WITH_TEXT_ATTACHMENTS, |
182 | + OPTION_TEST_PLAN_EXPORT, |
183 | ) |
184 | |
185 | def __init__(self, option_list=None, exporter_unit=None): |
186 | @@ -135,6 +137,10 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
187 | 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, |
188 | 'border': 1, 'bg_color': '#E6E6E6', |
189 | }) |
190 | + self.format06_2 = self.workbook.add_format({ |
191 | + 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, |
192 | + 'border': 1, 'bg_color': '#E6E6E6', 'bold': 1, |
193 | + }) |
194 | # Headlines (center) |
195 | self.format07 = self.workbook.add_format({ |
196 | 'align': 'center', 'size': 10, 'bold': 1, |
197 | @@ -385,7 +391,8 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
198 | self.format08 if i % 2 else self.format09 |
199 | ) |
200 | self.worksheet1.set_row( |
201 | - packages_starting_row + i, None, None, {'level': 1, 'hidden': True} |
202 | + packages_starting_row + i, None, None, |
203 | + {'level': 1, 'hidden': True} |
204 | ) |
205 | self.worksheet1.set_row( |
206 | packages_starting_row+len(data["resource_map"][resource]), |
207 | @@ -522,11 +529,11 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
208 | self.worksheet4.set_row( |
209 | self._lineno, 13, None, {'level': level}) |
210 | else: |
211 | - self.worksheet3.set_row(self._lineno, 13, None, |
212 | - {'collapsed': True}) |
213 | + self.worksheet3.set_row( |
214 | + self._lineno, 13, None, {'collapsed': True}) |
215 | if self.OPTION_WITH_DESCRIPTION in self._option_list: |
216 | - self.worksheet4.set_row(self._lineno, 13, None, |
217 | - {'collapsed': True}) |
218 | + self.worksheet4.set_row( |
219 | + self._lineno, 13, None, {'collapsed': True}) |
220 | self._write_job(children, result_map, max_level, level + 1) |
221 | else: |
222 | self.worksheet3.write( |
223 | @@ -604,8 +611,9 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
224 | self._lineno, 12 + 10.5 * desc_lines, |
225 | None, {'level': level, 'hidden': True}) |
226 | else: |
227 | - self.worksheet3.set_row(self._lineno, 12 + 10.5 * io_lines, |
228 | - None, {'hidden': True}) |
229 | + self.worksheet3.set_row( |
230 | + self._lineno, 12 + 10.5 * io_lines, |
231 | + None, {'hidden': True}) |
232 | if self.OPTION_WITH_DESCRIPTION in self._option_list: |
233 | self.worksheet4.set_row( |
234 | self._lineno, 12 + 10.5 * desc_lines, None, |
235 | @@ -649,6 +657,63 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
236 | self._lineno + 1, None, None, {'collapsed': True}) |
237 | self.worksheet3.autofilter(5, max_level, self._lineno, max_level + 3) |
238 | |
239 | + def write_tp_export(self, data): |
240 | + def _category_map(state): |
241 | + """Map from category id to their corresponding translated names.""" |
242 | + wanted_category_ids = frozenset({ |
243 | + job_state.effective_category_id |
244 | + for job_state in state.job_state_map.values() |
245 | + if job_state.job in state.run_list and |
246 | + job_state.job.plugin not in ("resource", "attachment") |
247 | + }) |
248 | + return { |
249 | + unit.id: unit.tr_name() |
250 | + for unit in state.unit_list |
251 | + if unit.Meta.name == 'category' |
252 | + and unit.id in wanted_category_ids |
253 | + } |
254 | + self.worksheet4.set_header( |
255 | + '&C{}'.format(data['manager'].test_plans[0])) |
256 | + self.worksheet4.set_footer('&CPage &P of &N') |
257 | + self.worksheet4.set_margins(left=0.3, right=0.3, top=0.5, bottom=0.5) |
258 | + self.worksheet4.set_column(0, 0, 40) |
259 | + self.worksheet4.set_column(1, 1, 13) |
260 | + self.worksheet4.set_column(2, 2, 55) |
261 | + self.worksheet4.write_row( |
262 | + 0, 0, |
263 | + ['Name', 'Certification status', 'Description'], self.format06_2 |
264 | + ) |
265 | + self.worksheet4.repeat_rows(0) |
266 | + self._lineno = 0 |
267 | + state = data['manager'].default_device_context.state |
268 | + cat_map = _category_map(state) |
269 | + run_list_ids = [job.id for job in state.run_list] |
270 | + for cat_id in sorted(cat_map, key=lambda x: cat_map[x].casefold()): |
271 | + for job_id in sorted(state._job_state_map): |
272 | + job_state = state._job_state_map[job_id] |
273 | + if job_id not in run_list_ids: |
274 | + continue |
275 | + if ( |
276 | + job_state.effective_category_id == cat_id and |
277 | + job_state.job.plugin not in ("resource", "attachment") |
278 | + ): |
279 | + self._lineno += 1 |
280 | + certification_status = \ |
281 | + job_state.effective_certification_status |
282 | + if certification_status == 'unspecified': |
283 | + certification_status = '' |
284 | + description = job_state.job.description |
285 | + if not description: |
286 | + description = job_state.job.summary |
287 | + self.worksheet4.write_row( |
288 | + self._lineno, 0, |
289 | + [job_state.job.partial_id, |
290 | + certification_status, description], |
291 | + self.format05) |
292 | + desc_lines = len(description.splitlines()) + 1 |
293 | + self.worksheet4.set_row(self._lineno, 12 * desc_lines) |
294 | + self._lineno += 1 |
295 | + |
296 | def write_attachments(self, data): |
297 | self.worksheet5.set_column(0, 0, 5) |
298 | self.worksheet5.set_column(1, 1, 120) |
299 | @@ -679,7 +744,8 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
300 | self.worksheet6.set_column(0, 0, 5) |
301 | self.worksheet6.set_column(1, 1, 120) |
302 | i = 4 |
303 | - for name in [job_id for job_id in data['result_map'] if data['result_map'][job_id]['plugin'] == 'resource']: |
304 | + for name in [job_id for job_id in data['result_map'] |
305 | + if data['result_map'][job_id]['plugin'] == 'resource']: |
306 | io_log = ' ' |
307 | try: |
308 | if data['result_map'][name]['io_log']: |
309 | @@ -704,6 +770,21 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
310 | self.worksheet6.set_row(i + j, None, None, {'collapsed': True}) |
311 | i += j + 1 # Insert a newline between resources logs |
312 | |
313 | + def dump_from_session_manager(self, session_manager, stream): |
314 | + """ |
315 | + Extract data from session_manager and dump it into the stream. |
316 | + |
317 | + :param session_manager: |
318 | + SessionManager instance that manages session to be exported by |
319 | + this exporter |
320 | + :param stream: |
321 | + Byte stream to write to. |
322 | + |
323 | + """ |
324 | + data = self.get_session_data_subset(session_manager) |
325 | + data['manager'] = session_manager |
326 | + self.dump(data, stream) |
327 | + |
328 | def dump(self, data, stream): |
329 | """ |
330 | Public method to dump the XLSX report to a stream |
331 | @@ -713,19 +794,27 @@ class XLSXSessionStateExporter(SessionStateExporterBase): |
332 | if self.OPTION_WITH_SYSTEM_INFO in self._option_list: |
333 | self.worksheet1 = self.workbook.add_worksheet(_('System Info')) |
334 | self.write_systeminfo(data) |
335 | - self.worksheet3 = self.workbook.add_worksheet(_('Test Results')) |
336 | - if self.OPTION_WITH_DESCRIPTION in self._option_list: |
337 | + if not self.OPTION_TEST_PLAN_EXPORT: |
338 | + self.worksheet3 = self.workbook.add_worksheet(_('Test Results')) |
339 | + if ( |
340 | + self.OPTION_WITH_DESCRIPTION in self._option_list or |
341 | + self.OPTION_TEST_PLAN_EXPORT in self._option_list |
342 | + ): |
343 | self.worksheet4 = self.workbook.add_worksheet( |
344 | _('Test Descriptions')) |
345 | - self.write_results(data) |
346 | + if self.OPTION_TEST_PLAN_EXPORT: |
347 | + self.write_tp_export(data) |
348 | + else: |
349 | + self.write_results(data) |
350 | if self.OPTION_WITH_SUMMARY in self._option_list: |
351 | self.worksheet2 = self.workbook.add_worksheet(_('Summary')) |
352 | self.write_summary(data) |
353 | if self.OPTION_WITH_TEXT_ATTACHMENTS in self._option_list: |
354 | self.worksheet5 = self.workbook.add_worksheet(_('Log Files')) |
355 | self.write_attachments(data) |
356 | - self.worksheet6 = self.workbook.add_worksheet(_('Resources Logs')) |
357 | - self.write_resources(data) |
358 | + if not self.OPTION_TEST_PLAN_EXPORT: |
359 | + self.worksheet6 = self.workbook.add_worksheet(_('Resources Logs')) |
360 | + self.write_resources(data) |
361 | for worksheet in self.workbook.worksheets(): |
362 | worksheet.outline_settings(True, False, False, True) |
363 | worksheet.hide_gridlines(2) |
364 | diff --git a/plainbox/impl/providers/exporters/units/exporter.pxu b/plainbox/impl/providers/exporters/units/exporter.pxu |
365 | index 65f96eb..c55843f 100644 |
366 | --- a/plainbox/impl/providers/exporters/units/exporter.pxu |
367 | +++ b/plainbox/impl/providers/exporters/units/exporter.pxu |
368 | @@ -45,3 +45,10 @@ _summary: Generate junit XML |
369 | entry_point: jinja2 |
370 | file_extension: junit.xml |
371 | data: {"template": "junit.xml"} |
372 | + |
373 | +unit: exporter |
374 | +id: tp-export |
375 | +_summary: Generate an Excel 2007+ XLSX export of a test plan |
376 | +entry_point: xlsx |
377 | +file_extension: xlsx |
378 | +options: tp-export |
379 | \ No newline at end of file |
380 | diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py |
381 | index 8adf4dc..7807a8f 100644 |
382 | --- a/plainbox/impl/runner.py |
383 | +++ b/plainbox/impl/runner.py |
384 | @@ -518,6 +518,7 @@ class JobRunner(IJobRunner): |
385 | else: |
386 | result = self._just_run_command( |
387 | job, job_state, config).get_result() |
388 | + |
389 | return result |
390 | |
391 | def run_manual_job(self, job, job_state, config): |
392 | @@ -970,3 +971,34 @@ class JobRunner(IJobRunner): |
393 | logger.warning( |
394 | _("Please store desired files in $PLAINBOX_SESSION_SHARE and" |
395 | " use regular temporary files for everything else")) |
396 | + |
397 | + |
398 | +class FakeJobRunner(JobRunner): |
399 | + |
400 | + """ |
401 | + Fake runner for jobs. |
402 | + |
403 | + Special runner that creates fake resource objects. |
404 | + """ |
405 | + |
406 | + def run_resource_job(self, job, job_state, config): |
407 | + """ |
408 | + Method called to run a job with plugin field equal to 'resource'. |
409 | + |
410 | + Only one resouce object is created from this runner. |
411 | + Exception: 'graphics_card' resource job creates two objects to |
412 | + simulate hybrid graphics. |
413 | + """ |
414 | + if job.plugin != "resource": |
415 | + # TRANSLATORS: please keep 'plugin' untranslated |
416 | + raise ValueError(_("bad job plugin value")) |
417 | + builder = JobResultBuilder() |
418 | + if job.partial_id == 'graphics_card': |
419 | + builder.io_log = [(0, 'stdout', b'a: b\n'), |
420 | + (1, 'stdout', b'\n'), |
421 | + (2, 'stdout', b'a: c\n')] |
422 | + else: |
423 | + builder.io_log = [(0, 'stdout', b'a: b\n')] |
424 | + builder.outcome = 'pass' |
425 | + builder.return_code = 0 |
426 | + return builder.get_result() |
427 | diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py |
428 | index 9470905..995e38b 100644 |
429 | --- a/plainbox/impl/session/assistant.py |
430 | +++ b/plainbox/impl/session/assistant.py |
431 | @@ -551,7 +551,7 @@ class SessionAssistant: |
432 | storage.remove() |
433 | |
434 | @raises(UnexpectedMethodCall) |
435 | - def start_new_session(self, title: str): |
436 | + def start_new_session(self, title: str, runner_cls=JobRunner): |
437 | """ |
438 | Create a new testing session. |
439 | |
440 | @@ -588,7 +588,7 @@ class SessionAssistant: |
441 | self._metadata.flags = {'bootstrapping'} |
442 | self._manager.checkpoint() |
443 | self._command_io_delegate = JobRunnerUIDelegate(_SilentUI()) |
444 | - self._init_runner() |
445 | + self._init_runner(runner_cls) |
446 | self.session_available(self._manager.storage.id) |
447 | _logger.debug("New session created: %s", title) |
448 | UsageExpectation.of(self).allowed_calls = { |
449 | @@ -1541,7 +1541,8 @@ class SessionAssistant: |
450 | |
451 | @raises(KeyError, OSError) |
452 | def export_to_file( |
453 | - self, exporter_id: str, option_list: 'list[str]', dir_path: str |
454 | + self, exporter_id: str, option_list: 'list[str]', dir_path: str, |
455 | + filename: str=None |
456 | ) -> str: |
457 | """ |
458 | Export the session to file using given exporter ID. |
459 | @@ -1556,6 +1557,9 @@ class SessionAssistant: |
460 | :param dir_path: |
461 | Path to the directory where session file should be written to. |
462 | Note that the file name is automatically generated, based on |
463 | + :param filename: |
464 | + Optional file name (without extension) |
465 | + By default, the file name is automatically generated, based on |
466 | creation time and type of exporter. |
467 | :returns: |
468 | Path to the written file. |
469 | @@ -1572,8 +1576,11 @@ class SessionAssistant: |
470 | # issues when copying files. |
471 | isoformat = "%Y-%m-%dT%H.%M.%S.%f" |
472 | timestamp = datetime.datetime.utcnow().strftime(isoformat) |
473 | + basename = 'submission_' + timestamp |
474 | + if filename: |
475 | + basename = filename |
476 | path = os.path.join(dir_path, ''.join( |
477 | - ['submission_', timestamp, '.', exporter.unit.file_extension])) |
478 | + [basename, '.', exporter.unit.file_extension])) |
479 | with open(path, 'wb') as stream: |
480 | exporter.dump_from_session_manager(self._manager, stream) |
481 | return path |
482 | @@ -1663,12 +1670,12 @@ class SessionAssistant: |
483 | self.finish_bootstrap: "to finish bootstrapping", |
484 | } |
485 | |
486 | - def _init_runner(self): |
487 | + def _init_runner(self, runner_cls): |
488 | self._execution_ctrl_list = [] |
489 | for ctrl_cls, args, kwargs in self._ctrl_setup_list: |
490 | self._execution_ctrl_list.append( |
491 | ctrl_cls(self._context.provider_list, *args, **kwargs)) |
492 | - self._runner = JobRunner( |
493 | + self._runner = runner_cls( |
494 | self._manager.storage.location, |
495 | self._context.provider_list, |
496 | jobs_io_log_dir=os.path.join( |
497 | diff --git a/plainbox/impl/session/state.py b/plainbox/impl/session/state.py |
498 | index 9360f91..9dfc15a 100644 |
499 | --- a/plainbox/impl/session/state.py |
500 | +++ b/plainbox/impl/session/state.py |
501 | @@ -799,6 +799,7 @@ class SessionState: |
502 | self._mandatory_job_list = [] |
503 | self._run_list = [] |
504 | self._resource_map = {} |
505 | + self._fake_resources = False |
506 | self._metadata = SessionMetaData() |
507 | super(SessionState, self).__init__() |
508 | |
509 | @@ -998,7 +999,8 @@ class SessionState: |
510 | any old entries), with a list of the resources that were parsed from |
511 | the IO log. |
512 | """ |
513 | - job.controller.observe_result(self, job, result) |
514 | + job.controller.observe_result( |
515 | + self, job, result, fake_resources=self._fake_resources) |
516 | self._recompute_job_readiness() |
517 | |
518 | @deprecated('0.9', 'use the add_unit() method instead') |
519 | @@ -1100,7 +1102,7 @@ class SessionState: |
520 | else: |
521 | # If there is a clash report DependencyDuplicateError only when the |
522 | # hashes are different. |
523 | - if new_job != existing_job: |
524 | + if new_job != existing_job and not self._fake_resources: |
525 | raise DependencyDuplicateError(existing_job, new_job) |
526 | self._add_job_siblings_unit(new_job, recompute, via) |
527 | return existing_job |
528 | diff --git a/plainbox/impl/unit/template.py b/plainbox/impl/unit/template.py |
529 | index 6e8a810..1103a76 100644 |
530 | --- a/plainbox/impl/unit/template.py |
531 | +++ b/plainbox/impl/unit/template.py |
532 | @@ -139,6 +139,7 @@ class TemplateUnit(Unit): |
533 | super().__init__( |
534 | data, raw_data, origin, provider, parameters, field_offset_map) |
535 | self._filter_program = None |
536 | + self._fake_resources = False |
537 | |
538 | @classmethod |
539 | def instantiate_template(cls, data, raw_data, origin, provider, parameters, |
540 | @@ -302,7 +303,7 @@ class TemplateUnit(Unit): |
541 | all_units.load() |
542 | return all_units.get_by_name(self.template_unit).plugin_object |
543 | |
544 | - def instantiate_all(self, resource_list): |
545 | + def instantiate_all(self, resource_list, fake_resources=False): |
546 | """ |
547 | Instantiate a list of job definitions. |
548 | |
549 | @@ -311,12 +312,15 @@ class TemplateUnit(Unit): |
550 | :param resource_list: |
551 | A list of resource objects with the correct name |
552 | (:meth:`template_resource`) |
553 | + :param fake_resources: |
554 | + An optional parameter to trigger test plan export execution mode |
555 | :returns: |
556 | A list of new Unit (or subclass) objects. |
557 | """ |
558 | unit_cls = self.get_target_unit_cls() |
559 | resources = [] |
560 | index = 0 |
561 | + self._fake_resources = fake_resources |
562 | for resource in resource_list: |
563 | if self.should_instantiate(resource): |
564 | index += 1 |
565 | @@ -380,6 +384,13 @@ class TemplateUnit(Unit): |
566 | # See https://bugs.launchpad.net/bugs/1561821 |
567 | parameters = { |
568 | k: v for k, v in parameters.items() if k in accessed_parameters} |
569 | + if self._fake_resources: |
570 | + parameters = {k: k.upper() for k in accessed_parameters} |
571 | + for k in parameters: |
572 | + if k.endswith('_slug'): |
573 | + parameters[k] = k.replace('_slug', '').upper() |
574 | + if 'index' in parameters: |
575 | + parameters['index'] = index |
576 | # Add the special __index__ to the resource namespace variables |
577 | parameters['__index__'] = index |
578 | # Instantiate the class using the instantiation API |
579 | @@ -401,6 +412,8 @@ class TemplateUnit(Unit): |
580 | specified resource object would make the filter program evaluate to |
581 | True. |
582 | """ |
583 | + if self._fake_resources: |
584 | + return True |
585 | program = self.get_filter_program() |
586 | if program is None: |
587 | return True |
I consider a bad design making the core of checkbox (JobRunner) aware of the TP export functionality and hardcoding a special case for graphics cards. If having specialised JobRunner is the best solution for the problem, let's do just that, by creating FakeJobRunner that overrides necessary parts. It could also help with the problem of propagating tp_export arg (which I would prefer renamed to exported_tp_name, tp_export misled me, but that's subjective).
I know this code does the job, but for me it has a "dirty hack" feel and voids abstractions that are already in place.