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
1=== added file 'qakit/practitest/all-suites.sh'
2--- qakit/practitest/all-suites.sh 1970-01-01 00:00:00 +0000
3+++ qakit/practitest/all-suites.sh 2015-08-10 20:32:55 +0000
4@@ -0,0 +1,9 @@
5+#!/bin/sh
6+
7+# LANG is required to avoid python unicode issues
8+export LANG=en_US.UTF-8
9+export PYTHONPATH=../..
10+for suite in 1294 1548 ; do
11+ perl -i.old -pe 's/^(PRACTITEST_PROJECT_ID) = (\d+)/$1 = '$suite'/;' config.ini
12+ ./pt-export.py $* -o /tmp/$suite.html
13+done
14
15=== modified file 'qakit/practitest/practitest.py'
16--- qakit/practitest/practitest.py 2015-06-30 14:44:59 +0000
17+++ qakit/practitest/practitest.py 2015-08-10 20:32:55 +0000
18@@ -17,7 +17,11 @@
19
20 import json
21 import logging
22+import os
23 import requests
24+import subprocess
25+import time
26+import xlrd
27
28 from pprint import pformat
29
30@@ -324,3 +328,125 @@
31 logging.debug(pformat(result))
32 logging.info('Updating issue pt:{} / lp:{}'.format(ptid, lpid))
33 return self._put(url, data=result)
34+
35+ def request_export(self, entity='Step'):
36+ """Ask the server to queue an export for all data of one type.
37+
38+ Will create a list of Issues, Tests , TestSets, Requirements or Steps,
39+ in a CSV format, to be able to export these entities to a regular
40+ spreadsheet. The method will put the export in the queue, and usually
41+ within couple of minutes it's ready to download.
42+
43+ Parameters:
44+ :param entity: one of the following: 'Issue', 'Test', 'TestSet',
45+ 'Requirement', 'Step'
46+
47+ Returns the following:
48+ id: the id of the export - a reference to receive the output,
49+ hours_gap: the # of hours until the user can re-create the same report
50+ hours_to_delete: the # of hours until the system will auto-delete this
51+ file
52+ """
53+ #print('request_export(%s)' % (entity))
54+ url = 'https://prod.practitest.com/api/exporter.json'
55+ data = dict(project_id=self.project_id, entity=entity)
56+ val = self._post(url, data).json()
57+ if 'error' in val:
58+ raise ValueError(val['error'])
59+ return val
60+
61+ def poll_export(self, id_):
62+ """Will check the status of a requested export.
63+ Parameters:
64+ :param id_: from the request_export method above
65+
66+ Returns:
67+ id: the id of this export,
68+ updated_at: the time it was updated
69+ status: the of this export (In Queue, working, completed),
70+ in_progress: true if it's not completed,
71+ url: the location to take the file from (via wget or curl)
72+ """
73+ print('poll_export(%s)' % (id_))
74+ url = 'https://prod.practitest.com/api/exporter/%s.json' % (id_)
75+ val = self._get(url).json()
76+ return val
77+
78+ def get_completed_export(self, url):
79+ """Download a completed export file and return its local filesystem path.
80+ """
81+ save_path = '/tmp/export.%s' % (time.strftime('%Y-%m-%d_%H:%M:%S'))
82+ #print('get_completed_export(%s) -> %s' % (url, save_path))
83+ cmd = ('wget', '-o', '/dev/null', '-O', save_path, url)
84+ subprocess.check_call(cmd)
85+ # PT sends a .xslx in a .zip file :(
86+ unzipped_dir = '%s.d' % (save_path)
87+ if not os.path.exists(unzipped_dir): os.makedirs(unzipped_dir)
88+ prev_dir = os.getcwd()
89+ os.chdir(unzipped_dir)
90+ cmd = ('unzip', save_path)
91+ subprocess.check_call(cmd)
92+ os.chdir(prev_dir)
93+ # find the unzipped .xlsx file
94+ matches = [x for x in os.listdir(unzipped_dir) if x.endswith('.xlsx')]
95+ if len(matches) != 1:
96+ raise ValueError('PT export file had unexpected contents.')
97+ xlsx_path = os.path.join(unzipped_dir, matches[0])
98+ #print('get_completed_export() done')
99+ return xlsx_path
100+
101+ def load_csv_export(self, path):
102+ """Load a .csv export file saved by get_completed_export().
103+ """
104+ #print('load_csv_export(%s)' % (path))
105+ wb = xlrd.open_workbook(path)
106+ names = wb.sheet_names()
107+ if len(names) != 1:
108+ raise ValueError('XLSX sheet layout not as expected')
109+ sheet = wb.sheet_by_index(0)
110+ return sheet
111+
112+ def request_and_wait_for_exports(self, entities=None, ids=None):
113+ #print('request_and_wait_for_exports(): %s' % (', '.join(entities)))
114+ if not entities: entities=[]
115+ if not ids: ids=[]
116+
117+ requested = []
118+ for i, entity in enumerate(entities):
119+ if len(ids) > i: # already downloaded, ID known
120+ continue
121+ d = dict(done=False, entity=entity)
122+ j = self.request_export(entity)
123+ print(j)
124+ for f in ('id', 'hours_gap', 'hours_to_delete'):
125+ d[f] = j[f]
126+ #print('request_export(%s): %s=%s' % (entity, f, d[f]))
127+ requested.append(d)
128+
129+ for i, id_ in enumerate(ids):
130+ if len(entities) > i: # already downloaded, ID known
131+ entity = entities[i]
132+ else:
133+ entity = 'unknown'
134+ d = dict(done=False, id=id_, entity=entity)
135+ requested.append(d)
136+
137+ while True:
138+ for d in requested:
139+ if not d['done']:
140+ j = self.poll_export(d['id'])
141+ for f in ('updated_at', 'status', 'in_progress', 'url'):
142+ d[f] = j[f]
143+ print('poll_export(%s): %s=%s' % (d['id'], f, d[f]))
144+ print('request_and_wait_for_export(%s): id=%s, status=%s'
145+ % (d['entity'], d['id'], d['status']))
146+ if not d['in_progress']:
147+ d['saved_path'] = self.get_completed_export(d['url'])
148+ d['data'] = self.load_csv_export(d['saved_path'])
149+ d['done'] = True
150+
151+ done = all([d['done'] for d in requested])
152+ if done: break
153+ time.sleep(5)
154+
155+ return requested
156
157=== added file 'qakit/practitest/pt-export.py'
158--- qakit/practitest/pt-export.py 1970-01-01 00:00:00 +0000
159+++ qakit/practitest/pt-export.py 2015-08-10 20:32:55 +0000
160@@ -0,0 +1,495 @@
161+#!/usr/bin/env python3
162+
163+# Export PractiTest data to HTML for viewing without an account
164+
165+# Copyright (C) 2015 Canonical
166+#
167+# Author: Selene Scriven
168+#
169+# This program is free software: you can redistribute it and/or modify
170+# it under the terms of the GNU General Public License as published by
171+# the Free Software Foundation, either version 3 of the License, or
172+# (at your option) any later version.
173+#
174+# This program is distributed in the hope that it will be useful,
175+# but WITHOUT ANY WARRANTY; without even the implied warranty of
176+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
177+# GNU General Public License for more details.
178+#
179+# You should have received a copy of the GNU General Public License
180+# along with this program. If not, see <http://www.gnu.org/licenses/>.
181+
182+import configparser
183+import os
184+import sys
185+
186+import practitest
187+
188+#from pprint import pprint
189+
190+cfg = None
191+
192+
193+def main(args):
194+ """pt-export: Export PractiTest data to plain HTML.
195+ Usage: pt-export [options] [filters]
196+ -h --help Show this help message and exit
197+ -c --cfg PATH Read config from PATH (default: config.ini)
198+ -o --out PATH Write output to PATH
199+ -r --regenerate Ask PT for a new export instead of previous one.
200+ --dump-extid Export external IDs in a tab-separated format.
201+
202+ Filters can be any of the following: (values are not case sensitive)
203+ Column=value Require test column to have an exact value.
204+ Column like value Require value to be somewhere in test column.
205+ any=value Require at least one column to have an exact value.
206+ any like value Require at least one column to contain value.
207+ value Same as 'any like value'.
208+ """
209+ global cfg
210+ cfg_path = 'config.ini'
211+ out_path = ''
212+ out_fmt = 'html'
213+ regenerate = False
214+ filters = []
215+
216+ # Very simple CLI, nothing fancy needed
217+ i = 0
218+ while i < len(args):
219+ a = args[i]
220+ if a in ('-h', '--help'):
221+ return usage()
222+ elif a in ('-c', '--cfg', '--config'):
223+ i += 1
224+ cfg_path = args[i]
225+ elif a in ('-o', '--out'):
226+ i += 1
227+ out_path = args[i]
228+ elif a in ('-r', '--regenerate'):
229+ regenerate = True
230+ elif a in ('--dump-extid',):
231+ out_fmt = 'extid'
232+ elif a in ('-u', '--force-unicode'):
233+ # TODO: Find a way to force UTF-8 for all I/O instead of ascii
234+ #os.environ['LANG'] = 'en_US.UTF-8'
235+ # Instead, run the script like this:
236+ # LANG=en_US.UTF-8 ./pt-export.py
237+ pass
238+ else:
239+ #return usage()
240+ filters.append(a)
241+ i += 1
242+
243+ cfg = read_config(cfg_path)
244+ if cfg['practitest_project_id'] not in ('1294', '1548'):
245+ raise ValueError('Unknown project ID: %s'
246+ % (cfg['practitest_project_id']))
247+ PT = practitest.PractitestSession(
248+ cfg['practitest_project_id'],
249+ cfg['practitest_api_key'],
250+ cfg['practitest_api_secret_key'],
251+ cfg['practitest_user_email'])
252+
253+ export_file(PT, out_path, regenerate, out_fmt, filters)
254+
255+
256+def usage():
257+ print(main.__doc__)
258+
259+
260+def export_file(PT, out_path, regenerate, out_fmt, filters):
261+ global cfg
262+
263+ print('Downloading tests...')
264+ if regenerate:
265+ exports = PT.request_and_wait_for_exports(entities=['Test'])
266+ else:
267+ if cfg['practitest_project_id'] == '1294':
268+ exports = PT.request_and_wait_for_exports(entities=['Test'],
269+ ids=[41])
270+ elif cfg['practitest_project_id'] == '1548':
271+ exports = PT.request_and_wait_for_exports(entities=['Test'],
272+ ids=[330])
273+
274+ # So, PT includes steps in 'Test' export, even though steps aren't
275+ # in the Test API... not necessary to load 'Step' export too.
276+ tests = exports[0]['data']
277+
278+ print('Parsing tests...')
279+ tests = interpret_test_sheet(tests)
280+
281+ print('Filtering tests...')
282+ tests = filter_tests(tests, filters)
283+
284+ print('Formatting tests...')
285+ if out_fmt == 'html':
286+ lines = exported_tests_to_html(tests)
287+ elif out_fmt == 'extid':
288+ lines = dump_extid(tests)
289+ else:
290+ raise ValueError('Unknown output format: %s' % out_fmt)
291+
292+ print('Saving...')
293+ if out_path:
294+ open(out_path, 'w').writelines(lines)
295+
296+
297+def filter_tests(tests, filters):
298+ """Interpret a list of filters, apply them to tests, return all matches.
299+ Return all tests by default.
300+ Filters are a list in any of the following formats:
301+ Column=value
302+ Column like value
303+ value
304+ The 'Column' part may be 'any' to match all columns.
305+ The plain 'value' format translates to 'any like value'.
306+ """
307+
308+ if not filters: return tests
309+
310+ # First, interpret the filters
311+ parsed_filters = []
312+ for f in filters:
313+ if '=' in f:
314+ left, right = f.split('=')
315+ right = right.lower()
316+ parsed_filters.append(('=', left, right))
317+ elif ' like ' in f:
318+ left, right = f.split(' like ')
319+ right = right.lower()
320+ parsed_filters.append(('like', left, right))
321+ else:
322+ parsed_filters.append(('like', 'any', f))
323+
324+ # Apply the filters
325+ filtered = []
326+ for test in tests:
327+ match = True
328+
329+ for type_, key, needle in parsed_filters:
330+ values = []
331+ if key == 'any':
332+ for key in test:
333+ values.append(str(test[key]).lower())
334+ else:
335+ if key not in test:
336+ match = False
337+ continue
338+ val = str(test[key]).lower()
339+ values = [val]
340+
341+ found = False
342+ for val in values:
343+ if type_ == '=':
344+ if val == needle:
345+ found = True
346+ elif type_ == 'like':
347+ if needle in val:
348+ found = True
349+ if not found:
350+ match = False
351+
352+ if match:
353+ filtered.append(test)
354+
355+ return filtered
356+
357+
358+def dump_extid(tests):
359+ """Make a report of test ID, external test ID, and test name
360+ (in a tab-separated format, nulls will be represented as '-')
361+ """
362+ lines = []
363+ columns = ('id', 'External ID', 'Name')
364+ for t in tests:
365+ fields = []
366+ for c in columns:
367+ v = str(t[c])
368+ if not v: v = '-'
369+ fields.append(v)
370+ line = '\t'.join(fields) + '\n'
371+ lines.append(line)
372+
373+ return lines
374+
375+
376+def interpret_test_sheet(tests):
377+ """Convert the PT xlsx export into a usable object tree.
378+ The input file is an awkward cross between a database and a spreadsheet,
379+ with related tables (Steps) broken out into an embedded sheet rectangle
380+ for each parent object... so it's a little awkward.
381+ """
382+
383+ # pull column names from sheet
384+ # save it into a format of test_columns[title] = (index, conversion func)
385+ test_columns = dict()
386+ columns = tests.row_values(0)
387+ for i, c in enumerate(columns):
388+ #print('col %i: %s' % (i, c))
389+ test_columns[c] = [i, str]
390+
391+ def int_or_none(x):
392+ if x == '':
393+ return None
394+ else:
395+ return int(x)
396+
397+ # convert non-string columns
398+ test_columns['id'][1] = int_or_none
399+ test_columns['Step position'][1] = int_or_none
400+
401+ def tests_with_steps(tests):
402+ merged = []
403+ for i in range(1, tests.nrows):
404+ row = tests.row_values(i)
405+ rowlen = tests.row_len(i)
406+
407+ # is this is a continuation of the previous row?
408+ if row[0]:
409+ t = dict()
410+ merged.append(t)
411+ t['steps'] = [] # start with no test steps
412+ child = False
413+ else:
414+ t = merged[-1]
415+ child = True
416+
417+ step = dict()
418+ for name, (col, func) in test_columns.items():
419+ # skip empty child cells
420+ if child and not row[col]:
421+ continue
422+ try:
423+ val = func(row[col])
424+ except Exception as e:
425+ print('Warn: row %i conversion error: %s' % (i, str(e)))
426+ val = row[col]
427+ if name.startswith('Step '):
428+ step[name] = val
429+ else:
430+ if child:
431+ t[name] = t[name] + '\n' + val
432+ else:
433+ t[name] = val
434+ if ('Step position' in step) \
435+ and (step['Step position'] is not None):
436+ t['steps'].append(step)
437+
438+ #print('%s: %s (%i steps)' % (t['id'], t['Name'], len(t['steps'])))
439+ #pprint(t['steps'])
440+
441+ return merged
442+
443+ all_tests = tests_with_steps(tests)
444+
445+ return all_tests
446+
447+
448+def exported_tests_to_html(tests):
449+
450+ def cr_to_br(t):
451+ return t.replace('\n', '<br />')
452+
453+ def nobr(t):
454+ return str(t).replace(' ', '&nbsp;')
455+
456+ def step_field(step, key):
457+ if key in step:
458+ val = step[key]
459+ if isinstance(val, str):
460+ val = cr_to_br(val)
461+ return val
462+ else:
463+ return ''
464+
465+ def fmt_steps(steps, test, tr_class):
466+ fmt_steps.counter += 1
467+ lines = []
468+ if len(steps) > 0:
469+ label = '%i&nbsp;steps' % (len(steps))
470+ else:
471+ label = '...'
472+ lines.append('%s <br />' % (label))
473+ hide_id = 'steps-%s' % (fmt_steps.counter)
474+ #lines.append('''<span class="clickable" '
475+ # 'onclick="toggle_display('%s')">%s</span><br />'''
476+ # % (hide_id, label))
477+ lines.append('</td></tr>')
478+ lines.append('<tr class="%s child"><td></td><td></td><td colspan="%s">'
479+ % (tr_class, len(columns) - 2))
480+
481+ lines.append('<table class="steps" id="%s"><tr><td>' % (hide_id))
482+
483+ for extra in ['External ID', 'Description', 'Pre-requisites and setup',
484+ 'Whiteboard', 'Comments']:
485+ val = get_field(test, extra)
486+ if val and (val != 'N/A'):
487+ lines.append('<b>%s</b>:<br />%s<br />'
488+ % (extra, cr_to_br(val)))
489+
490+ if len(steps) > 0:
491+ lines.append('<b>Steps</b>:<br />')
492+ lines.append('<table>')
493+ lines.append('<tr>')
494+ for header in ('#', '', 'desc', 'result'):
495+ lines.append('<th align="left">%s</th>' % (header))
496+ lines.append('</tr>')
497+ i = 0
498+ for step in steps:
499+ i += 1
500+ lines.append('<tr class="step-%s">' % (i % 2))
501+ for field in ('Step position', 'Step name',
502+ 'Step description', 'Step expected_results'):
503+ lines.append('<td>%s </td>' % step_field(step, field))
504+ lines.append('</tr>')
505+ lines.append('</table>')
506+
507+ lines.append('</td></tr></table>')
508+ return '\n'.join(lines)
509+ fmt_steps.counter = 0
510+
511+ def fmt_automated(x):
512+ if x.lower() == 'yes': return 'automated'
513+ else: return 'manual'
514+
515+ if cfg['practitest_project_id'] == '1294':
516+ columns = [
517+ ('id', 'ID', str, False),
518+ ('Test Level', 'Suite', str, False),
519+ ('Subsystem', 'Category', nobr, False),
520+ ('Component', 'Subcat', str, False),
521+ ('Name', 'Title', str, False),
522+ ('Status', 'Status', str, False),
523+ ('Tags', 'Tags', str, False),
524+ ('Devices', 'Devices', str, False),
525+ ('Release', 'Release', str, False),
526+ ('Automated', 'Automated', fmt_automated, False),
527+ ('steps', 'Steps', fmt_steps, True),
528+ ]
529+ elif cfg['practitest_project_id'] == '1548':
530+ columns = [
531+ ('id', 'ID', str, False),
532+ ('Test Level', 'Suite', str, False),
533+ ('Image Part', 'Part', str, False),
534+ ('Test Domain', 'Domain', str, False),
535+ ('Application', 'App', str, False),
536+ ('Name', 'Title', str, False),
537+ ('Status', 'Status', str, False),
538+ ('Tags', 'Tags', str, False),
539+ ('Devices', 'Devices', str, False),
540+ ('Release', 'Release', str, False),
541+ ('Automated', 'Automated', fmt_automated, False),
542+ ('steps', 'Steps', fmt_steps, True),
543+ ]
544+
545+ # Sort by various fields...
546+ if cfg['practitest_project_id'] == '1294':
547+ foo = [(
548+ t['Test Level'] or '',
549+ t['Subsystem'] or '',
550+ t['Component'] or '',
551+ t['id'], t) for t in tests]
552+ elif cfg['practitest_project_id'] == '1548':
553+ foo = [(
554+ t['Test Level'] or '',
555+ t['Test Domain'] or '',
556+ t['Image Part'] or '',
557+ t['Application'] or '',
558+ t['id'], t) for t in tests]
559+ foo.sort()
560+ tests = [x[-1] for x in foo]
561+
562+ lines = []
563+ lines.append('<html>')
564+ lines.append('<head>')
565+ lines.append('<title>All Tests</title>')
566+ lines.append('<style><!--')
567+ lines.append(open('style.css').read())
568+ lines.append('--></style>')
569+ lines.append('<script>')
570+ lines.append(open('scripts.js').read())
571+ lines.append('</script>')
572+ lines.append('</head>')
573+ lines.append('<body>')
574+
575+ lines.append('Filter:')
576+ lines.append('<input type="search" class="light-table-filter" '
577+ 'name="filter-string" data-table="test-table" '
578+ 'placeholder="search string"></input>')
579+
580+ lines.append(' - ')
581+ lines.append('<span id="numtests">%i</span> tests' % (len(tests)))
582+ lines.append(' - ')
583+ lines.append('<span class="clickable" onclick="toggle_all_steps(1)">'
584+ 'Expand All</span>')
585+ lines.append(' - ')
586+ lines.append('<span class="clickable" onclick="toggle_all_steps(0)">'
587+ 'Collapse All</span>')
588+
589+ lines.append('<hr />')
590+
591+ lines.append('<table id="all" class="test-table table">')
592+
593+ def table_header():
594+ lines.append('<thead><tr>')
595+ for field, title, func, full in columns:
596+ lines.append('<th align="left">%s</th>' % (title))
597+ lines.append('</tr></thead>')
598+
599+ def none_to_empty(v):
600+ if v is None:
601+ return ''
602+ return v
603+
604+ def get_field(test, key):
605+ if key in test:
606+ return none_to_empty(test[key])
607+ return None
608+
609+ table_header()
610+ lines.append('<tbody>')
611+ i = 0
612+ for test in tests:
613+ i += 1
614+ #if (i%25) == 1: # repeat header every 25 tests
615+ # table_header()
616+ tr_class = 'tests-%s' % (i % 2)
617+ hide_id = 'steps-%s' % (fmt_steps.counter + 1)
618+ lines.append('''<tr class="%s" onclick="toggle_display('%s')">'''
619+ % (tr_class, hide_id))
620+ for field, title, func, full in columns:
621+ val = get_field(test, field)
622+ if full:
623+ val = func(val, test, tr_class)
624+ else:
625+ val = func(val)
626+ # the extra space here prevents a bug where
627+ # a search for "emul" would match "Device</td><td>Multimedia"
628+ lines.append('<td>{} </td>'.format(val))
629+ lines.append('</tr>')
630+
631+ lines.append('</tbody>')
632+ lines.append('</table>')
633+
634+ lines.append('</body>')
635+ lines.append('</html>')
636+
637+ return lines
638+
639+
640+def read_config(path, section='DEFAULT'):
641+ """Get values from config.ini as a dict (env vars override file)"""
642+ # Read config as dict
643+ cfg = configparser.ConfigParser()
644+ cfg.read(path)
645+ cfg = dict(cfg[section])
646+ # let env vars override config file
647+ for k in cfg:
648+ if k.upper() in os.environ:
649+ cfg[k] = os.environ[k.upper()]
650+ return cfg
651+
652+
653+if __name__ == "__main__":
654+ import sys
655+ main(sys.argv[1:])
656
657=== added file 'qakit/practitest/scripts.js'
658--- qakit/practitest/scripts.js 1970-01-01 00:00:00 +0000
659+++ qakit/practitest/scripts.js 2015-08-10 20:32:55 +0000
660@@ -0,0 +1,97 @@
661+function show(target) {
662+ document.getElementById(target).style.display = 'block';
663+}
664+
665+function hide(target) {
666+ document.getElementById(target).style.display = 'none';
667+}
668+
669+function toggle_display(target) {
670+ if (document.getElementById(target).style.display == 'block') {
671+ hide(target);
672+ } else {
673+ show(target);
674+ }
675+}
676+
677+function toggle_all_steps(expand) {
678+ for (i=1; ; i++) {
679+ target = "steps-" + i;
680+ if (document.getElementById(target)) {
681+ if (expand == 1) {
682+ show(target);
683+ } else if (expand == 0) {
684+ hide(target);
685+ } else if (expand == -1) {
686+ toggle_display(target);
687+ }
688+ } else {
689+ break;
690+ }
691+ }
692+}
693+
694+(function(document) {
695+ 'use strict';
696+
697+ var LightTableFilter = (function(Arr) {
698+
699+ var _input;
700+ var matches = 0;
701+
702+ function _onInputEvent(e) {
703+ _input = e.target;
704+ matches = 0;
705+ var tables = document.getElementsByClassName(_input.getAttribute('data-table'));
706+ Arr.forEach.call(tables, function(table) {
707+ Arr.forEach.call(table.tBodies, function(tbody) {
708+ // rows come in pairs; main row and collapse-able details
709+ var row;
710+ for(row=0; row<tbody.rows.length; row+=2) {
711+ _filter(tbody.rows[row],tbody.rows[row+1]);
712+ }
713+ });
714+ });
715+ document.getElementById("numtests").innerHTML = matches;
716+ }
717+
718+ function _filter(row1,row2) {
719+ var text = row1.textContent.toLowerCase()
720+ + row2.textContent.toLowerCase();
721+ // search for each word separately; require all words to match
722+ var vals = _input.value.toLowerCase().split(' ');
723+ var show = 1;
724+ var i, val;
725+ for(i=0; i<vals.length; i++) {
726+ val = vals[i];
727+ if(text.indexOf(val) < 0) { show = 0; }
728+ }
729+ // count how many items will be visible
730+ if (show) { matches ++; }
731+ // hide non-matching rows
732+ row1.style.display = show ? 'table-row' : 'none';
733+ row2.style.display = row1.style.display;
734+ // preserve alternating row colors
735+ var newclass = "tests-" + (matches%2);
736+ row1.setAttribute('class', newclass);
737+ row2.setAttribute('class', newclass);
738+ }
739+
740+ return {
741+ init: function() {
742+ var inputs = document.getElementsByClassName('light-table-filter');
743+ Arr.forEach.call(inputs, function(input) {
744+ input.oninput = _onInputEvent;
745+ });
746+ }
747+ };
748+ })(Array.prototype);
749+
750+ document.addEventListener('readystatechange', function() {
751+ if (document.readyState === 'complete') {
752+ LightTableFilter.init();
753+ }
754+ });
755+
756+})(document);
757+
758
759=== added file 'qakit/practitest/style.css'
760--- qakit/practitest/style.css 1970-01-01 00:00:00 +0000
761+++ qakit/practitest/style.css 2015-08-10 20:32:55 +0000
762@@ -0,0 +1,50 @@
763+table {
764+ border: none;
765+ border-collapse: collapse;
766+}
767+
768+#all {
769+}
770+
771+#all th {
772+ background-color: #ffeedd;
773+}
774+
775+#all td {
776+ vertical-align: top;
777+ padding-right: 10px;
778+}
779+
780+.tests-0 td {
781+ background-color: #eeeeee;
782+}
783+
784+.tests-1 td {
785+ background-color: #ffffff;
786+}
787+
788+.steps {
789+ display: none;
790+}
791+
792+.steps th {
793+ background-color: #ffffff;
794+}
795+
796+.steps, .steps td {
797+ border: none;
798+ border-bottom: none;
799+}
800+
801+.steps .step-0 td {
802+ background-color: #dddddd;
803+}
804+
805+.steps .step-1 td {
806+ background-color: #eeeeee;
807+}
808+
809+.clickable {
810+ text-decoration: underline;
811+ color: #4444cc;
812+}

Subscribers

People subscribed via source and target branches