Author: janne.t.harkonen
Date: Thu May 21 08:39:50 2009
New Revision: 2008
Added:
trunk/proto/testgen/
trunk/proto/testgen/test/
trunk/proto/testgen/test/template.tsv
trunk/proto/testgen/test/test_testgen.py (contents, props changed)
trunk/proto/testgen/test/vars.tsv
trunk/proto/testgen/testgen.py (contents, props changed)
Log:
Prototype for a tool that enables creation of data driven test suites
Added: trunk/proto/testgen/test/template.tsv
==============================================================================
--- (empty file)
+++ trunk/proto/testgen/test/template.tsv Thu May 21 08:39:50 2009
@@ -0,0 +1,24 @@
+* Settings *
+Meta: Foo Bar
+Library OperatingSystem
+Resource resource.tsv
+Variables vars.py
+
+* Variables *
+${SCALAR} foo
+...@{list} 1 2 3 4
+
+* Test Cases *
+
+Test
+ [ Documentation ] This is test doc
+ [ Setup ] ${EMPTY}
+ Keyword 1 ${arg2}
+ Keyword 2 ${arg1}
+ [ Teardown ] My TD
+
+* keywords *
+
+Keyword 1 [Arguments] ${arg1} ${arg2}=foo
+ Log ${arg1}
+ [ Return ] ${arg2}
Added: trunk/proto/testgen/test/test_testgen.py
==============================================================================
--- (empty file)
+++ trunk/proto/testgen/test/test_testgen.py Thu May 21 08:39:50 2009
@@ -0,0 +1,24 @@
+import unittest
+from StringIO import StringIO
+
+from os.path import dirname, join
+import sys
+sys.path.insert(0, join(dirname(__file__), '..'))
+
+from testgen import VariableIterator
+
+
+class TestVariableIterator(unittest.TestCase):
+
+ def test_creation(self):
+ vars = VariableIterator(StringIO('* variables
*\n${var1}\t${var2}\nval1\tval2\n'))
+ self.assertEqual(vars._variable_mapping, {'${var1}': 0, '${var2}':
1})
+
+ def test_iteration(self):
+ vars = VariableIterator(StringIO('* variables
*\n${var1}\t${var2}\nval1.1\tval2.1\nval1.2\tval2.2\n'))
+ for var, exp in zip(vars,
[{'${var1}': 'val1.1', '${var2}': 'val2.1'},
{'${var1}': 'val1.2', '${var2}': 'val2.2'}]):
+ self.assertEqual(var, exp)
+
+
+if __name__ == '__main__':
+ unittest.main()
Added: trunk/proto/testgen/test/vars.tsv
==============================================================================
--- (empty file)
+++ trunk/proto/testgen/test/vars.tsv Thu May 21 08:39:50 2009
@@ -0,0 +1,5 @@
+* variables *
+${arg1} ${arg2}
+val1.1 val2.1
+val1.2 val2.2
+val1.3 val2.3
Added: trunk/proto/testgen/testgen.py
==============================================================================
--- (empty file)
+++ trunk/proto/testgen/testgen.py Thu May 21 08:39:50 2009
@@ -0,0 +1,339 @@
+"""A tool for creating data driven test case for Robot Framework
+
+Usage: testgen.py variablefile template output
+
+This script reads the variable and template files and generates a test
suite
+which has all test cases found in the template multiplied with all the
rows of
+the variable file. Suite settings, variables and user keywords from the
template
+file are serialized as is.
+
+Currently, the input files must be in tsv (tab separated values) format.
Also
+the output file is written in tsv. The variables file must have a format
+demonstrated in the example below, e.g. header row, followed by a row with
the
+names of the variables, and on the subsequent rows the values for the
+variables.
+
+Options:
+ -h -? --help Print this usage instruction.
+
+Example:
+<<template.tsv>>
+* Settings *
+Documentation Example data driven suite
+* Test Cases *
+Example Test Keyword ${arg1} ${arg2}
+* User Keywords *
+Keyword [Arguments] ${val1} ${val2}
+ Log Many ${val1} ${val2}
+
+<<variables.tsv>>
+* Variables *
+${arg1} ${arg2}
+value1 value2
+value11 value22
+
+Given above files, command
+python testgen.py variables.tsv template.tsv output.tsv
+produces following test suite:
+
+<<output.tsv>>
+* Settings *
+Documentation Example data driven suite
+* Test Cases *
+Example Test 1 Keyword value1 value2
+Example Test 2 Keyword value11 value22
+* User Keywords *
+Keyword [Arguments] ${val1} ${val2}
+ Log Many ${val1} ${val2}
+"""
+import sys
+import os
+import csv
+
+from robot.parsing.model import FileSuite
+from robot.parsing.tsvreader import TsvReader
+from robot.errors import DataError, Information
+from robot import utils
+
+
+class TestGeneratingSuite(FileSuite):
+
+ def serialize(self, variables, serializer):
+ self._serialize_settings(serializer)
+ self._serialize_variables(serializer)
+ self._serialize_tests(variables, serializer)
+ self._serialize_keywords(serializer)
+
+ def _serialize_settings(self, serializer):
+ serializer.start_settings()
+ if self.doc:
+ serializer.setting('Documentation', self.doc)
+ for name, value in self.metadata.items():
+ serializer.setting('Meta: %s' % name, [value])
+ for name in ['Default Tags', 'Force Tags', 'Suite Setup',
+ 'Suite Teardown', 'Test Setup', 'Test Teardown',
+ ]:
+ value = self._get_setting(self, name)
+ if value:
+ serializer.setting(name, value)
+ for imp in self.imports:
+ serializer.setting(imp.name, imp._item.value)
+ serializer.end_settings()
+
+ def _serialize_variables(self, serializer):
+ serializer.start_variables()
+ for var in self.variables:
+ serializer.variable(var.name, var.value)
+ serializer.end_variables()
+
+ def _serialize_tests(self, variables, serializer):
+ serializer.start_testcases()
+ for test in self.tests:
+ orig_name = test.name
+ for index, vars in enumerate(variables):
+ test.name = '%s %d' % (orig_name, (index+1))
+ serializer.start_testcase(test)
+ if test.doc:
+ serializer.setting('Documentation', [test.doc])
+ for name in ['Setup', 'Tags', 'Timeout']:
+ value = self._get_setting(test, name)
+ if value is not None:
+ serializer.setting(name, value)
+ for kw in test.keywords:
+ data = self._replace_variables(vars, [kw.name] +
kw.args)
+ serializer.keyword(data)
+ if test.teardown is not None:
+ serializer.setting('Teardown', test.teardown)
+ serializer.end_testcase()
+ serializer.end_testcases()
+
+ def _serialize_keywords(self, serializer):
+ serializer.start_keywords()
+ for uk in self.user_keywords:
+ serializer.start_keyword(uk)
+ args = self._format_args(uk.args, uk.defaults, uk.varargs)
+ if args:
+ serializer.setting('Arguments', args)
+ if uk.doc:
+ serializer.setting('Documentation', uk.doc)
+ if uk.timeout is not None:
+ serializer.setting('Timeout', uk.timeout)
+ for kw in uk.keywords:
+ serializer.keyword([kw.name] + kw.args)
+ if uk.return_value:
+ serializer.setting('Return Value', uk.return_value)
+ serializer.end_keywords()
+
+ def _replace_variables(self, variables, data):
+ replaced = []
+ for elem in data:
+ for key in variables:
+ if key in elem:
+ elem = elem.replace(key, variables[key])
+ replaced.append(elem)
+ return replaced
+
+ def _get_setting(self, item, name):
+ return getattr(item, name.lower().replace(' ', '_'))
+
+ def _format_args(self, args, defaults, varargs):
+ parsed = []
+ if args:
+ parsed.extend(list(args))
+ if defaults:
+ for i, value in enumerate(defaults):
+ index = len(args) - len(defaults) + i
+ parsed[index] = parsed[index] + '=' + value
+ if varargs:
+ parsed.append(varargs)
+ return parsed
+
+
+class VariableIterator(object):
+
+ def __init__(self, varfile):
+ self._variable_mapping = {}
+ self._variables = []
+ TsvReader().read(varfile, self)
+
+ def __iter__(self):
+ while self._variables:
+ data = self._variables.pop(0)
+ values = {}
+ for key in self._variable_mapping:
+ values[key] = data[self._variable_mapping[key]]
+ yield values
+
+ def start_table(self, name):
+ return name.lower().strip() == 'variables'
+
+ def add_row(self, row):
+ if not self._variable_mapping:
+ for pos in range(len(row)):
+ self._variable_mapping[row[pos]] = pos
+ else:
+ self._variables.append(row)
+
+
+class AbstractFileWriter(object):
+
+ def __init__(self, path, cols):
+ self._output = open(path, 'wb')
+ self._cols = cols
+ self._tc_name = None
+ self._uk_name = None
+
+ def start_settings(self):
+ self._write_header_row(['Setting', 'Value'])
+
+ def end_settings(self):
+ self._write_empty_row()
+
+ def start_variables(self):
+ self._write_header_row(['Variable', 'Value'])
+
+ def end_variables(self):
+ self._write_empty_row()
+
+ def start_testcases(self):
+ self._write_header_row(['Test Case', 'Action', 'Argument'])
+
+ def end_testcases(self):
+ self._write_empty_row()
+
+ def start_testcase(self, testcase):
+ self._tc_name = testcase.name
+
+ def end_testcase(self):
+ if self._tc_name:
+ self._write_normal_row([self._tc_name])
+ self._tc_name = None
+ self._write_empty_row()
+
+ def start_keywords(self):
+ self._write_header_row(['Keyword', 'Action', 'Argument'])
+
+ def end_keywords(self):
+ self._write_empty_row()
+ self._output.close()
+
+ def start_keyword(self, userkeyword):
+ self._uk_name = userkeyword.name
+
+ def end_keyword(self):
+ if self._uk_name:
+ self._write_normal_row([self._uk_name])
+ self._uk_name = None
+ self._write_empty_row()
+
+ def setting(self, name, value):
+ if self._tc_name is None and self._uk_name is None:
+ self._write_normal_row([name] + value)
+ else: # TC and UK settings
+ row = [self._get_tc_or_uk_name(), '[%s]' % name] + value
+ self._write_normal_row(row, indent=1)
+
+ def variable(self, name, value):
+ self._write_normal_row([name] + value)
+
+ def keyword(self, keyword):
+ name = self._get_tc_or_uk_name()
+ # TODO: When adding support for PARALLEL, FOR, etc. need to use
+ # different indent when inside indented block
+ self._write_normal_row([name] + keyword, indent=1)
+
+ def _write_header_row(self, row):
+ row += [row[-1]] * (self._cols - len(row))
+ self._write_header_row_impl(row)
+
+ def _write_normal_row(self, row, indent=0):
+ firstrow = True
+ while True:
+ if firstrow:
+ current = row[:self._cols]
+ row = row[self._cols:]
+ firstrow = False
+ else:
+ current = ['']*indent + ['...'] + row[:self._cols-indent-1]
+ row = row[self._cols-indent-1:]
+ self._escape_empty_trailing_cells(current)
+ current += [''] * (self._cols - len(current))
+ self._write_normal_row_impl(current)
+ if not row:
+ break
+
+ def _write_empty_row(self):
+ self._write_normal_row([])
+
+ def _escape_empty_trailing_cells(self, row):
+ if len(row) > 0 and row[-1] == '':
+ row[-1] = '\\'
+
+ def _get_title(self, path):
+ dire, base = os.path.split(path)
+ if base.lower() == '__init__.html':
+ path = dire
+ return utils.printable_name_from_path(path)
+
+ def _write_header_row_impl(self, row):
+ raise NotImplementedError
+
+ def _write_normal_row_impl(self, row):
+ raise NotImplementedError
+
+
+class TsvFileWriter(AbstractFileWriter):
+
+ def __init__(self, path):
+ AbstractFileWriter.__init__(self, path, 8)
+ self._writer = csv.writer(self._output, dialect='excel-tab')
+
+ def _write_header_row_impl(self, row):
+ self._writer.writerow(['*%s*' % cell for cell in row])
+
+ def _write_normal_row_impl(self, row):
+ self._writer.writerow([cell.encode('UTF-8') for cell in row])
+
+ def _get_tc_or_uk_name(self):
+ if self._tc_name:
+ name = self._tc_name
+ self._tc_name = ''
+ elif self._uk_name:
+ name = self._uk_name
+ self._uk_name = ''
+ else:
+ name = ''
+ return name
+
+
+def generate_suite(cliargs):
+ opts, (varfile, templatefile, outfile) = _process_args(cliargs)
+ suite = TestGeneratingSuite(templatefile)
+ vars = VariableIterator(open(varfile))
+ if not outfile.endswith('tsv'):
+ outfile = outfile + '.tsv'
+ suite.serialize(vars, TsvFileWriter(outfile))
+
+def _process_args(cliargs):
+ ap = utils.ArgumentParser(__doc__, arg_limits=(3, sys.maxint))
+ try:
+ opts, paths = ap.parse_args(cliargs, help='help', check_args=True)
+ except Information, msg:
+ exit(msg=str(msg))
+ except DataError, err:
+ exit(error=str(err))
+ return opts, paths
+
+def exit(rc=0, error=None, msg=None):
+ if error:
+ print error, "\n\nUse '--help' option to get usage information."
+ if rc == 0:
+ rc = 255
+ if msg:
+ print msg
+ rc = 1
+ sys.exit(rc)
+
+
+if __name__ == '__main__':
+ generate_suite(sys.argv[1:])