Merge lp:~toykeeper/qakit/add-pt-to-html into lp:qakit

Proposed by Selene ToyKeeper
Status: Needs review
Proposed branch: lp:~toykeeper/qakit/add-pt-to-html
Merge into: lp:qakit
Diff against target: 812 lines (+777/-0)
5 files modified
qakit/practitest/all-suites.sh (+9/-0)
qakit/practitest/practitest.py (+126/-0)
qakit/practitest/pt-export.py (+495/-0)
qakit/practitest/scripts.js (+97/-0)
qakit/practitest/style.css (+50/-0)
To merge this branch: bzr merge lp:~toykeeper/qakit/add-pt-to-html
Reviewer Review Type Date Requested Status
Canonical Platform QA Team Pending
Review via email: mp+259446@code.launchpad.net

Commit message

Added a PractiTest suite exporter for easy viewing of cases.

Description of the change

Added a PractiTest suite exporter for easy viewing of cases.

To post a comment you must log in.
Revision history for this message
Selene ToyKeeper (toykeeper) wrote :

Probably not ready for actual merging yet, but it does function.
Mostly needs cleanup.

lp:~toykeeper/qakit/add-pt-to-html updated
12. By Selene ToyKeeper

Made details much wider (is its own table row, made some code more complex),
made entire row clickable to expand/collapse details,
made search filter each word individually,
added expand/collapse all links.

13. By Selene ToyKeeper

Added -r / --regenerate CLI option.
PT can only export every 2 hours, so it helps to be able to control
whether the export is re-requested or not.

14. By Selene ToyKeeper

Added support for new test suite schema. Choose a schema based on project ID.
Added a count of matching tests in current view.

15. By Selene ToyKeeper

Fixed pep8 lint complaints.
(except for the ones relating to vim's indent settings
 and commented-out lines of code)

16. By Selene ToyKeeper

Small bug fix in filter function; could match strings which spanned multiple html tags.
Allowed linewrap in "domain" column.
Minor code cleaning.

17. By Selene ToyKeeper

Added a script to export all current PT projects.

18. By Selene ToyKeeper

Ensure row colors will still alternate after a search.

Revision history for this message
Allan LeSage (allanlesage) wrote :

This is big! I'm looking through but will need a more thorough review than I'm able to give presently.

Revision history for this message
Selene ToyKeeper (toykeeper) wrote :

It might help to first merge bzr+ssh://bazaar.launchpad.net/~allanlesage/qakit/qakit/ , which this was branched from. The diff should then become smaller and easier to read.

Revision history for this message
Max Brustkern (nuclearbob) wrote :

I'm not sure how to do it, but if you set that branch as a prerequisite, it'll only show the new diff.

Revision history for this message
Brendan Donegan (brendan-donegan) wrote :

You need to resubmit the MP and can specify the prerequisite there

On Fri, May 22, 2015 at 10:17 PM, Max Brustkern <<email address hidden>
> wrote:

> I'm not sure how to do it, but if you set that branch as a prerequisite,
> it'll only show the new diff.
> --
> https://code.launchpad.net/~toykeeper/qakit/add-pt-to-html/+merge/259446
> Your team Canonical Platform QA Team is requested to review the proposed
> merge of lp:~toykeeper/qakit/add-pt-to-html into lp:qakit.
>

lp:~toykeeper/qakit/add-pt-to-html updated
19. By Selene ToyKeeper

Added ability to filter during conversion, and to dump external IDs.
Made it easier to filter client-side html by automated status.

20. By Selene ToyKeeper

Renamed pt-to-html to pt-export.

21. By Selene ToyKeeper

merged updates from trunk

Unmerged revisions

21. By Selene ToyKeeper

merged updates from trunk

20. By Selene ToyKeeper

Renamed pt-to-html to pt-export.

19. By Selene ToyKeeper

Added ability to filter during conversion, and to dump external IDs.
Made it easier to filter client-side html by automated status.

18. By Selene ToyKeeper

Ensure row colors will still alternate after a search.

17. By Selene ToyKeeper

Added a script to export all current PT projects.

16. By Selene ToyKeeper

Small bug fix in filter function; could match strings which spanned multiple html tags.
Allowed linewrap in "domain" column.
Minor code cleaning.

15. By Selene ToyKeeper

Fixed pep8 lint complaints.
(except for the ones relating to vim's indent settings
 and commented-out lines of code)

14. By Selene ToyKeeper

Added support for new test suite schema. Choose a schema based on project ID.
Added a count of matching tests in current view.

13. By Selene ToyKeeper

Added -r / --regenerate CLI option.
PT can only export every 2 hours, so it helps to be able to control
whether the export is re-requested or not.

12. By Selene ToyKeeper

Made details much wider (is its own table row, made some code more complex),
made entire row clickable to expand/collapse details,
made search filter each word individually,
added expand/collapse all links.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'qakit/practitest/all-suites.sh'
--- qakit/practitest/all-suites.sh 1970-01-01 00:00:00 +0000
+++ qakit/practitest/all-suites.sh 2015-08-10 20:32:55 +0000
@@ -0,0 +1,9 @@
1#!/bin/sh
2
3# LANG is required to avoid python unicode issues
4export LANG=en_US.UTF-8
5export PYTHONPATH=../..
6for suite in 1294 1548 ; do
7 perl -i.old -pe 's/^(PRACTITEST_PROJECT_ID) = (\d+)/$1 = '$suite'/;' config.ini
8 ./pt-export.py $* -o /tmp/$suite.html
9done
010
=== modified file 'qakit/practitest/practitest.py'
--- qakit/practitest/practitest.py 2015-06-30 14:44:59 +0000
+++ qakit/practitest/practitest.py 2015-08-10 20:32:55 +0000
@@ -17,7 +17,11 @@
1717
18import json18import json
19import logging19import logging
20import os
20import requests21import requests
22import subprocess
23import time
24import xlrd
2125
22from pprint import pformat26from pprint import pformat
2327
@@ -324,3 +328,125 @@
324 logging.debug(pformat(result))328 logging.debug(pformat(result))
325 logging.info('Updating issue pt:{} / lp:{}'.format(ptid, lpid))329 logging.info('Updating issue pt:{} / lp:{}'.format(ptid, lpid))
326 return self._put(url, data=result)330 return self._put(url, data=result)
331
332 def request_export(self, entity='Step'):
333 """Ask the server to queue an export for all data of one type.
334
335 Will create a list of Issues, Tests , TestSets, Requirements or Steps,
336 in a CSV format, to be able to export these entities to a regular
337 spreadsheet. The method will put the export in the queue, and usually
338 within couple of minutes it's ready to download.
339
340 Parameters:
341 :param entity: one of the following: 'Issue', 'Test', 'TestSet',
342 'Requirement', 'Step'
343
344 Returns the following:
345 id: the id of the export - a reference to receive the output,
346 hours_gap: the # of hours until the user can re-create the same report
347 hours_to_delete: the # of hours until the system will auto-delete this
348 file
349 """
350 #print('request_export(%s)' % (entity))
351 url = 'https://prod.practitest.com/api/exporter.json'
352 data = dict(project_id=self.project_id, entity=entity)
353 val = self._post(url, data).json()
354 if 'error' in val:
355 raise ValueError(val['error'])
356 return val
357
358 def poll_export(self, id_):
359 """Will check the status of a requested export.
360 Parameters:
361 :param id_: from the request_export method above
362
363 Returns:
364 id: the id of this export,
365 updated_at: the time it was updated
366 status: the of this export (In Queue, working, completed),
367 in_progress: true if it's not completed,
368 url: the location to take the file from (via wget or curl)
369 """
370 print('poll_export(%s)' % (id_))
371 url = 'https://prod.practitest.com/api/exporter/%s.json' % (id_)
372 val = self._get(url).json()
373 return val
374
375 def get_completed_export(self, url):
376 """Download a completed export file and return its local filesystem path.
377 """
378 save_path = '/tmp/export.%s' % (time.strftime('%Y-%m-%d_%H:%M:%S'))
379 #print('get_completed_export(%s) -> %s' % (url, save_path))
380 cmd = ('wget', '-o', '/dev/null', '-O', save_path, url)
381 subprocess.check_call(cmd)
382 # PT sends a .xslx in a .zip file :(
383 unzipped_dir = '%s.d' % (save_path)
384 if not os.path.exists(unzipped_dir): os.makedirs(unzipped_dir)
385 prev_dir = os.getcwd()
386 os.chdir(unzipped_dir)
387 cmd = ('unzip', save_path)
388 subprocess.check_call(cmd)
389 os.chdir(prev_dir)
390 # find the unzipped .xlsx file
391 matches = [x for x in os.listdir(unzipped_dir) if x.endswith('.xlsx')]
392 if len(matches) != 1:
393 raise ValueError('PT export file had unexpected contents.')
394 xlsx_path = os.path.join(unzipped_dir, matches[0])
395 #print('get_completed_export() done')
396 return xlsx_path
397
398 def load_csv_export(self, path):
399 """Load a .csv export file saved by get_completed_export().
400 """
401 #print('load_csv_export(%s)' % (path))
402 wb = xlrd.open_workbook(path)
403 names = wb.sheet_names()
404 if len(names) != 1:
405 raise ValueError('XLSX sheet layout not as expected')
406 sheet = wb.sheet_by_index(0)
407 return sheet
408
409 def request_and_wait_for_exports(self, entities=None, ids=None):
410 #print('request_and_wait_for_exports(): %s' % (', '.join(entities)))
411 if not entities: entities=[]
412 if not ids: ids=[]
413
414 requested = []
415 for i, entity in enumerate(entities):
416 if len(ids) > i: # already downloaded, ID known
417 continue
418 d = dict(done=False, entity=entity)
419 j = self.request_export(entity)
420 print(j)
421 for f in ('id', 'hours_gap', 'hours_to_delete'):
422 d[f] = j[f]
423 #print('request_export(%s): %s=%s' % (entity, f, d[f]))
424 requested.append(d)
425
426 for i, id_ in enumerate(ids):
427 if len(entities) > i: # already downloaded, ID known
428 entity = entities[i]
429 else:
430 entity = 'unknown'
431 d = dict(done=False, id=id_, entity=entity)
432 requested.append(d)
433
434 while True:
435 for d in requested:
436 if not d['done']:
437 j = self.poll_export(d['id'])
438 for f in ('updated_at', 'status', 'in_progress', 'url'):
439 d[f] = j[f]
440 print('poll_export(%s): %s=%s' % (d['id'], f, d[f]))
441 print('request_and_wait_for_export(%s): id=%s, status=%s'
442 % (d['entity'], d['id'], d['status']))
443 if not d['in_progress']:
444 d['saved_path'] = self.get_completed_export(d['url'])
445 d['data'] = self.load_csv_export(d['saved_path'])
446 d['done'] = True
447
448 done = all([d['done'] for d in requested])
449 if done: break
450 time.sleep(5)
451
452 return requested
327453
=== added file 'qakit/practitest/pt-export.py'
--- qakit/practitest/pt-export.py 1970-01-01 00:00:00 +0000
+++ qakit/practitest/pt-export.py 2015-08-10 20:32:55 +0000
@@ -0,0 +1,495 @@
1#!/usr/bin/env python3
2
3# Export PractiTest data to HTML for viewing without an account
4
5# Copyright (C) 2015 Canonical
6#
7# Author: Selene Scriven
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22import configparser
23import os
24import sys
25
26import practitest
27
28#from pprint import pprint
29
30cfg = None
31
32
33def main(args):
34 """pt-export: Export PractiTest data to plain HTML.
35 Usage: pt-export [options] [filters]
36 -h --help Show this help message and exit
37 -c --cfg PATH Read config from PATH (default: config.ini)
38 -o --out PATH Write output to PATH
39 -r --regenerate Ask PT for a new export instead of previous one.
40 --dump-extid Export external IDs in a tab-separated format.
41
42 Filters can be any of the following: (values are not case sensitive)
43 Column=value Require test column to have an exact value.
44 Column like value Require value to be somewhere in test column.
45 any=value Require at least one column to have an exact value.
46 any like value Require at least one column to contain value.
47 value Same as 'any like value'.
48 """
49 global cfg
50 cfg_path = 'config.ini'
51 out_path = ''
52 out_fmt = 'html'
53 regenerate = False
54 filters = []
55
56 # Very simple CLI, nothing fancy needed
57 i = 0
58 while i < len(args):
59 a = args[i]
60 if a in ('-h', '--help'):
61 return usage()
62 elif a in ('-c', '--cfg', '--config'):
63 i += 1
64 cfg_path = args[i]
65 elif a in ('-o', '--out'):
66 i += 1
67 out_path = args[i]
68 elif a in ('-r', '--regenerate'):
69 regenerate = True
70 elif a in ('--dump-extid',):
71 out_fmt = 'extid'
72 elif a in ('-u', '--force-unicode'):
73 # TODO: Find a way to force UTF-8 for all I/O instead of ascii
74 #os.environ['LANG'] = 'en_US.UTF-8'
75 # Instead, run the script like this:
76 # LANG=en_US.UTF-8 ./pt-export.py
77 pass
78 else:
79 #return usage()
80 filters.append(a)
81 i += 1
82
83 cfg = read_config(cfg_path)
84 if cfg['practitest_project_id'] not in ('1294', '1548'):
85 raise ValueError('Unknown project ID: %s'
86 % (cfg['practitest_project_id']))
87 PT = practitest.PractitestSession(
88 cfg['practitest_project_id'],
89 cfg['practitest_api_key'],
90 cfg['practitest_api_secret_key'],
91 cfg['practitest_user_email'])
92
93 export_file(PT, out_path, regenerate, out_fmt, filters)
94
95
96def usage():
97 print(main.__doc__)
98
99
100def export_file(PT, out_path, regenerate, out_fmt, filters):
101 global cfg
102
103 print('Downloading tests...')
104 if regenerate:
105 exports = PT.request_and_wait_for_exports(entities=['Test'])
106 else:
107 if cfg['practitest_project_id'] == '1294':
108 exports = PT.request_and_wait_for_exports(entities=['Test'],
109 ids=[41])
110 elif cfg['practitest_project_id'] == '1548':
111 exports = PT.request_and_wait_for_exports(entities=['Test'],
112 ids=[330])
113
114 # So, PT includes steps in 'Test' export, even though steps aren't
115 # in the Test API... not necessary to load 'Step' export too.
116 tests = exports[0]['data']
117
118 print('Parsing tests...')
119 tests = interpret_test_sheet(tests)
120
121 print('Filtering tests...')
122 tests = filter_tests(tests, filters)
123
124 print('Formatting tests...')
125 if out_fmt == 'html':
126 lines = exported_tests_to_html(tests)
127 elif out_fmt == 'extid':
128 lines = dump_extid(tests)
129 else:
130 raise ValueError('Unknown output format: %s' % out_fmt)
131
132 print('Saving...')
133 if out_path:
134 open(out_path, 'w').writelines(lines)
135
136
137def filter_tests(tests, filters):
138 """Interpret a list of filters, apply them to tests, return all matches.
139 Return all tests by default.
140 Filters are a list in any of the following formats:
141 Column=value
142 Column like value
143 value
144 The 'Column' part may be 'any' to match all columns.
145 The plain 'value' format translates to 'any like value'.
146 """
147
148 if not filters: return tests
149
150 # First, interpret the filters
151 parsed_filters = []
152 for f in filters:
153 if '=' in f:
154 left, right = f.split('=')
155 right = right.lower()
156 parsed_filters.append(('=', left, right))
157 elif ' like ' in f:
158 left, right = f.split(' like ')
159 right = right.lower()
160 parsed_filters.append(('like', left, right))
161 else:
162 parsed_filters.append(('like', 'any', f))
163
164 # Apply the filters
165 filtered = []
166 for test in tests:
167 match = True
168
169 for type_, key, needle in parsed_filters:
170 values = []
171 if key == 'any':
172 for key in test:
173 values.append(str(test[key]).lower())
174 else:
175 if key not in test:
176 match = False
177 continue
178 val = str(test[key]).lower()
179 values = [val]
180
181 found = False
182 for val in values:
183 if type_ == '=':
184 if val == needle:
185 found = True
186 elif type_ == 'like':
187 if needle in val:
188 found = True
189 if not found:
190 match = False
191
192 if match:
193 filtered.append(test)
194
195 return filtered
196
197
198def dump_extid(tests):
199 """Make a report of test ID, external test ID, and test name
200 (in a tab-separated format, nulls will be represented as '-')
201 """
202 lines = []
203 columns = ('id', 'External ID', 'Name')
204 for t in tests:
205 fields = []
206 for c in columns:
207 v = str(t[c])
208 if not v: v = '-'
209 fields.append(v)
210 line = '\t'.join(fields) + '\n'
211 lines.append(line)
212
213 return lines
214
215
216def interpret_test_sheet(tests):
217 """Convert the PT xlsx export into a usable object tree.
218 The input file is an awkward cross between a database and a spreadsheet,
219 with related tables (Steps) broken out into an embedded sheet rectangle
220 for each parent object... so it's a little awkward.
221 """
222
223 # pull column names from sheet
224 # save it into a format of test_columns[title] = (index, conversion func)
225 test_columns = dict()
226 columns = tests.row_values(0)
227 for i, c in enumerate(columns):
228 #print('col %i: %s' % (i, c))
229 test_columns[c] = [i, str]
230
231 def int_or_none(x):
232 if x == '':
233 return None
234 else:
235 return int(x)
236
237 # convert non-string columns
238 test_columns['id'][1] = int_or_none
239 test_columns['Step position'][1] = int_or_none
240
241 def tests_with_steps(tests):
242 merged = []
243 for i in range(1, tests.nrows):
244 row = tests.row_values(i)
245 rowlen = tests.row_len(i)
246
247 # is this is a continuation of the previous row?
248 if row[0]:
249 t = dict()
250 merged.append(t)
251 t['steps'] = [] # start with no test steps
252 child = False
253 else:
254 t = merged[-1]
255 child = True
256
257 step = dict()
258 for name, (col, func) in test_columns.items():
259 # skip empty child cells
260 if child and not row[col]:
261 continue
262 try:
263 val = func(row[col])
264 except Exception as e:
265 print('Warn: row %i conversion error: %s' % (i, str(e)))
266 val = row[col]
267 if name.startswith('Step '):
268 step[name] = val
269 else:
270 if child:
271 t[name] = t[name] + '\n' + val
272 else:
273 t[name] = val
274 if ('Step position' in step) \
275 and (step['Step position'] is not None):
276 t['steps'].append(step)
277
278 #print('%s: %s (%i steps)' % (t['id'], t['Name'], len(t['steps'])))
279 #pprint(t['steps'])
280
281 return merged
282
283 all_tests = tests_with_steps(tests)
284
285 return all_tests
286
287
288def exported_tests_to_html(tests):
289
290 def cr_to_br(t):
291 return t.replace('\n', '<br />')
292
293 def nobr(t):
294 return str(t).replace(' ', '&nbsp;')
295
296 def step_field(step, key):
297 if key in step:
298 val = step[key]
299 if isinstance(val, str):
300 val = cr_to_br(val)
301 return val
302 else:
303 return ''
304
305 def fmt_steps(steps, test, tr_class):
306 fmt_steps.counter += 1
307 lines = []
308 if len(steps) > 0:
309 label = '%i&nbsp;steps' % (len(steps))
310 else:
311 label = '...'
312 lines.append('%s <br />' % (label))
313 hide_id = 'steps-%s' % (fmt_steps.counter)
314 #lines.append('''<span class="clickable" '
315 # 'onclick="toggle_display('%s')">%s</span><br />'''
316 # % (hide_id, label))
317 lines.append('</td></tr>')
318 lines.append('<tr class="%s child"><td></td><td></td><td colspan="%s">'
319 % (tr_class, len(columns) - 2))
320
321 lines.append('<table class="steps" id="%s"><tr><td>' % (hide_id))
322
323 for extra in ['External ID', 'Description', 'Pre-requisites and setup',
324 'Whiteboard', 'Comments']:
325 val = get_field(test, extra)
326 if val and (val != 'N/A'):
327 lines.append('<b>%s</b>:<br />%s<br />'
328 % (extra, cr_to_br(val)))
329
330 if len(steps) > 0:
331 lines.append('<b>Steps</b>:<br />')
332 lines.append('<table>')
333 lines.append('<tr>')
334 for header in ('#', '', 'desc', 'result'):
335 lines.append('<th align="left">%s</th>' % (header))
336 lines.append('</tr>')
337 i = 0
338 for step in steps:
339 i += 1
340 lines.append('<tr class="step-%s">' % (i % 2))
341 for field in ('Step position', 'Step name',
342 'Step description', 'Step expected_results'):
343 lines.append('<td>%s </td>' % step_field(step, field))
344 lines.append('</tr>')
345 lines.append('</table>')
346
347 lines.append('</td></tr></table>')
348 return '\n'.join(lines)
349 fmt_steps.counter = 0
350
351 def fmt_automated(x):
352 if x.lower() == 'yes': return 'automated'
353 else: return 'manual'
354
355 if cfg['practitest_project_id'] == '1294':
356 columns = [
357 ('id', 'ID', str, False),
358 ('Test Level', 'Suite', str, False),
359 ('Subsystem', 'Category', nobr, False),
360 ('Component', 'Subcat', str, False),
361 ('Name', 'Title', str, False),
362 ('Status', 'Status', str, False),
363 ('Tags', 'Tags', str, False),
364 ('Devices', 'Devices', str, False),
365 ('Release', 'Release', str, False),
366 ('Automated', 'Automated', fmt_automated, False),
367 ('steps', 'Steps', fmt_steps, True),
368 ]
369 elif cfg['practitest_project_id'] == '1548':
370 columns = [
371 ('id', 'ID', str, False),
372 ('Test Level', 'Suite', str, False),
373 ('Image Part', 'Part', str, False),
374 ('Test Domain', 'Domain', str, False),
375 ('Application', 'App', str, False),
376 ('Name', 'Title', str, False),
377 ('Status', 'Status', str, False),
378 ('Tags', 'Tags', str, False),
379 ('Devices', 'Devices', str, False),
380 ('Release', 'Release', str, False),
381 ('Automated', 'Automated', fmt_automated, False),
382 ('steps', 'Steps', fmt_steps, True),
383 ]
384
385 # Sort by various fields...
386 if cfg['practitest_project_id'] == '1294':
387 foo = [(
388 t['Test Level'] or '',
389 t['Subsystem'] or '',
390 t['Component'] or '',
391 t['id'], t) for t in tests]
392 elif cfg['practitest_project_id'] == '1548':
393 foo = [(
394 t['Test Level'] or '',
395 t['Test Domain'] or '',
396 t['Image Part'] or '',
397 t['Application'] or '',
398 t['id'], t) for t in tests]
399 foo.sort()
400 tests = [x[-1] for x in foo]
401
402 lines = []
403 lines.append('<html>')
404 lines.append('<head>')
405 lines.append('<title>All Tests</title>')
406 lines.append('<style><!--')
407 lines.append(open('style.css').read())
408 lines.append('--></style>')
409 lines.append('<script>')
410 lines.append(open('scripts.js').read())
411 lines.append('</script>')
412 lines.append('</head>')
413 lines.append('<body>')
414
415 lines.append('Filter:')
416 lines.append('<input type="search" class="light-table-filter" '
417 'name="filter-string" data-table="test-table" '
418 'placeholder="search string"></input>')
419
420 lines.append(' - ')
421 lines.append('<span id="numtests">%i</span> tests' % (len(tests)))
422 lines.append(' - ')
423 lines.append('<span class="clickable" onclick="toggle_all_steps(1)">'
424 'Expand All</span>')
425 lines.append(' - ')
426 lines.append('<span class="clickable" onclick="toggle_all_steps(0)">'
427 'Collapse All</span>')
428
429 lines.append('<hr />')
430
431 lines.append('<table id="all" class="test-table table">')
432
433 def table_header():
434 lines.append('<thead><tr>')
435 for field, title, func, full in columns:
436 lines.append('<th align="left">%s</th>' % (title))
437 lines.append('</tr></thead>')
438
439 def none_to_empty(v):
440 if v is None:
441 return ''
442 return v
443
444 def get_field(test, key):
445 if key in test:
446 return none_to_empty(test[key])
447 return None
448
449 table_header()
450 lines.append('<tbody>')
451 i = 0
452 for test in tests:
453 i += 1
454 #if (i%25) == 1: # repeat header every 25 tests
455 # table_header()
456 tr_class = 'tests-%s' % (i % 2)
457 hide_id = 'steps-%s' % (fmt_steps.counter + 1)
458 lines.append('''<tr class="%s" onclick="toggle_display('%s')">'''
459 % (tr_class, hide_id))
460 for field, title, func, full in columns:
461 val = get_field(test, field)
462 if full:
463 val = func(val, test, tr_class)
464 else:
465 val = func(val)
466 # the extra space here prevents a bug where
467 # a search for "emul" would match "Device</td><td>Multimedia"
468 lines.append('<td>{} </td>'.format(val))
469 lines.append('</tr>')
470
471 lines.append('</tbody>')
472 lines.append('</table>')
473
474 lines.append('</body>')
475 lines.append('</html>')
476
477 return lines
478
479
480def read_config(path, section='DEFAULT'):
481 """Get values from config.ini as a dict (env vars override file)"""
482 # Read config as dict
483 cfg = configparser.ConfigParser()
484 cfg.read(path)
485 cfg = dict(cfg[section])
486 # let env vars override config file
487 for k in cfg:
488 if k.upper() in os.environ:
489 cfg[k] = os.environ[k.upper()]
490 return cfg
491
492
493if __name__ == "__main__":
494 import sys
495 main(sys.argv[1:])
0496
=== added file 'qakit/practitest/scripts.js'
--- qakit/practitest/scripts.js 1970-01-01 00:00:00 +0000
+++ qakit/practitest/scripts.js 2015-08-10 20:32:55 +0000
@@ -0,0 +1,97 @@
1function show(target) {
2 document.getElementById(target).style.display = 'block';
3}
4
5function hide(target) {
6 document.getElementById(target).style.display = 'none';
7}
8
9function toggle_display(target) {
10 if (document.getElementById(target).style.display == 'block') {
11 hide(target);
12 } else {
13 show(target);
14 }
15}
16
17function toggle_all_steps(expand) {
18 for (i=1; ; i++) {
19 target = "steps-" + i;
20 if (document.getElementById(target)) {
21 if (expand == 1) {
22 show(target);
23 } else if (expand == 0) {
24 hide(target);
25 } else if (expand == -1) {
26 toggle_display(target);
27 }
28 } else {
29 break;
30 }
31 }
32}
33
34(function(document) {
35 'use strict';
36
37 var LightTableFilter = (function(Arr) {
38
39 var _input;
40 var matches = 0;
41
42 function _onInputEvent(e) {
43 _input = e.target;
44 matches = 0;
45 var tables = document.getElementsByClassName(_input.getAttribute('data-table'));
46 Arr.forEach.call(tables, function(table) {
47 Arr.forEach.call(table.tBodies, function(tbody) {
48 // rows come in pairs; main row and collapse-able details
49 var row;
50 for(row=0; row<tbody.rows.length; row+=2) {
51 _filter(tbody.rows[row],tbody.rows[row+1]);
52 }
53 });
54 });
55 document.getElementById("numtests").innerHTML = matches;
56 }
57
58 function _filter(row1,row2) {
59 var text = row1.textContent.toLowerCase()
60 + row2.textContent.toLowerCase();
61 // search for each word separately; require all words to match
62 var vals = _input.value.toLowerCase().split(' ');
63 var show = 1;
64 var i, val;
65 for(i=0; i<vals.length; i++) {
66 val = vals[i];
67 if(text.indexOf(val) < 0) { show = 0; }
68 }
69 // count how many items will be visible
70 if (show) { matches ++; }
71 // hide non-matching rows
72 row1.style.display = show ? 'table-row' : 'none';
73 row2.style.display = row1.style.display;
74 // preserve alternating row colors
75 var newclass = "tests-" + (matches%2);
76 row1.setAttribute('class', newclass);
77 row2.setAttribute('class', newclass);
78 }
79
80 return {
81 init: function() {
82 var inputs = document.getElementsByClassName('light-table-filter');
83 Arr.forEach.call(inputs, function(input) {
84 input.oninput = _onInputEvent;
85 });
86 }
87 };
88 })(Array.prototype);
89
90 document.addEventListener('readystatechange', function() {
91 if (document.readyState === 'complete') {
92 LightTableFilter.init();
93 }
94 });
95
96})(document);
97
098
=== added file 'qakit/practitest/style.css'
--- qakit/practitest/style.css 1970-01-01 00:00:00 +0000
+++ qakit/practitest/style.css 2015-08-10 20:32:55 +0000
@@ -0,0 +1,50 @@
1table {
2 border: none;
3 border-collapse: collapse;
4}
5
6#all {
7}
8
9#all th {
10 background-color: #ffeedd;
11}
12
13#all td {
14 vertical-align: top;
15 padding-right: 10px;
16}
17
18.tests-0 td {
19 background-color: #eeeeee;
20}
21
22.tests-1 td {
23 background-color: #ffffff;
24}
25
26.steps {
27 display: none;
28}
29
30.steps th {
31 background-color: #ffffff;
32}
33
34.steps, .steps td {
35 border: none;
36 border-bottom: none;
37}
38
39.steps .step-0 td {
40 background-color: #dddddd;
41}
42
43.steps .step-1 td {
44 background-color: #eeeeee;
45}
46
47.clickable {
48 text-decoration: underline;
49 color: #4444cc;
50}

Subscribers

People subscribed via source and target branches