[GRASS-SVN] r61489 - grass/trunk/lib/python/gunittest

svn_grass at osgeo.org svn_grass at osgeo.org
Thu Jul 31 13:32:00 PDT 2014


Author: wenzeslaus
Date: 2014-07-31 13:32:00 -0700 (Thu, 31 Jul 2014)
New Revision: 61489

Modified:
   grass/trunk/lib/python/gunittest/__init__.py
   grass/trunk/lib/python/gunittest/checkers.py
   grass/trunk/lib/python/gunittest/invoker.py
   grass/trunk/lib/python/gunittest/loader.py
   grass/trunk/lib/python/gunittest/main.py
   grass/trunk/lib/python/gunittest/multireport.py
   grass/trunk/lib/python/gunittest/reporters.py
Log:
gunittest: new command line interface for main using argparse, additional info to test run main key-value file, do not fail when test cannot be imported, only try to import grass modules if possible to improve behavior when not running in proper GRASS session

Modified: grass/trunk/lib/python/gunittest/__init__.py
===================================================================
--- grass/trunk/lib/python/gunittest/__init__.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/__init__.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -15,5 +15,21 @@
 by Vaclav Petras as a student and Soeren Gebbert as a mentor.
 """
 
-from .case import TestCase
-from .main import test
+from __future__ import print_function
+
+try:
+    from .case import TestCase
+    from .main import test
+except ImportError, e:
+    print('WARNING: Cannot import ({e.message}).\n'
+          'Ignoring the failed import because it does not harm if you need'
+          ' something different'
+          ' from gunittest. Probably the environment is not set properly'
+          ' (e.g. dynamic libraries are not available and ctypes-based modules'
+          ' cannot work).'.format(e=e))
+    # we need to ignore import errors for the cases when we just need
+    # gunittest for reports and ctypes are not available (or the environment
+    # is not set properly)
+    # .main probably does not need to be checked but it imports a lot of
+    # things, so it might be hard to keep track in the future
+    # .case imports PyGRASS which imports ctypes modules in its __init__.py

Modified: grass/trunk/lib/python/gunittest/checkers.py
===================================================================
--- grass/trunk/lib/python/gunittest/checkers.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/checkers.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -16,8 +16,19 @@
 import sys
 import re
 import doctest
-import grass.script.core as gcore
 
+try:
+    from grass.script.core import KeyValue
+except (ImportError, AttributeError):
+    # TODO: we are silent about the error and use a object with different
+    # interface, should be replaced by central keyvalue module
+    # this can happen when translations are not available
+    # TODO: grass should survive are give better error when tranlsations are not available
+    # even the lazy loading after firts _ call would be interesting
+    # File "...grass/script/core.py", line 40, in <module>
+    # AttributeError: 'NoneType' object has no attribute 'endswith'
+    KeyValue = dict
+
 # alternative term to check(er(s)) would be compare
 
 
@@ -153,7 +164,7 @@
         use ``lambda x: x`` for no conversion
 
     :return: a dictionary representation of text
-    :return type: grass.script.core.KeyValue
+    :return type: grass.script.core.KeyValue or dict
 
     And example of converting text with text, floats, integers and list
     to a dictionary::
@@ -172,7 +183,7 @@
     # splitting according to universal newlines approach
     # TODO: add also general split with vsep
     text = text.splitlines()
-    kvdict = gcore.KeyValue()
+    kvdict = KeyValue()
     functions = [] if functions is None else functions
 
     for line in text:

Modified: grass/trunk/lib/python/gunittest/invoker.py
===================================================================
--- grass/trunk/lib/python/gunittest/invoker.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/invoker.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -30,6 +30,10 @@
                         NoopFileAnonymizer, keyvalue_to_text)
 from .utils import silent_rmtree, ensure_dir
 
+# needed for write_gisrc
+# TODO: it would be good to find some way of writing rc without the need to
+# have GRASS proprly set (anything from grass.script requires translations to
+# be set, i.e. the GRASS environment properly set)
 import grass.script.setup as gsetup
 
 import collections
@@ -56,6 +60,7 @@
         keyval['status'] = 'failed' if returncode else 'passed'
     keyval['returncode'] = returncode
     keyval['test_file_authors'] = test_file_authors
+
     with open(filename, 'w') as keyval_file:
         keyval_file.write(keyvalue_to_text(keyval))
     return keyval
@@ -168,7 +173,7 @@
         if self.clean_mapsets:
             shutil.rmtree(mapset_dir)
 
-    def run_in_location(self, gisdbase, location, location_shortcut,
+    def run_in_location(self, gisdbase, location, location_type,
                         results_dir):
         """Run tests in a given location"""
         if os.path.abspath(results_dir) == os.path.abspath(self.start_dir):
@@ -179,12 +184,13 @@
                 GrassTestFilesTextReporter(stream=sys.stderr),
                 GrassTestFilesHtmlReporter(
                     file_anonymizer=self._file_anonymizer),
-                GrassTestFilesKeyValueReporter()
+                GrassTestFilesKeyValueReporter(
+                    info=dict(location=location, location_type=location_type))
             ])
         self.testsuite_dirs = collections.defaultdict(list)  # reset list of dirs each time
         # TODO: move constants out of loader class or even module
         modules = discover_modules(start_dir=self.start_dir,
-                                   grass_location=location_shortcut,
+                                   grass_location=location_type,
                                    file_pattern=GrassTestLoader.files_in_testsuite,
                                    skip_dirs=GrassTestLoader.skip_dirs,
                                    testsuite_dir=GrassTestLoader.testsuite_dir,

Modified: grass/trunk/lib/python/gunittest/loader.py
===================================================================
--- grass/trunk/lib/python/gunittest/loader.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/loader.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -31,7 +31,7 @@
 def discover_modules(start_dir, file_pattern, skip_dirs, testsuite_dir,
                      grass_location,
                      all_locations_value, universal_location_value,
-                     import_modules):
+                     import_modules, add_failed_imports=True):
     """Find all test files (modules) in a directory tree.
 
     The function is designed specifically for GRASS testing framework
@@ -46,7 +46,7 @@
     :param skip_dirs: directories not to recurse to (e.g. ``.svn``)
     :param testsuite_dir: name of directory where the test files are found,
         the function will not recurse to this directory
-    :param grass_location: string with an accepted location (shortcut)
+    :param grass_location: string with an accepted location type (category, shortcut)
     :param all_locations_value: string used to say that all locations
         should be loaded (grass_location can be set to this value)
     :param universal_location_value: string marking a test as
@@ -86,9 +86,10 @@
                 # everything was loaded into Python
                 abspath = os.path.abspath(full)
                 sys.path.insert(0, abspath)
+                add = False
                 try:
                     m = importlib.import_module(name)
-                    add = False
+                    # TODO: now we are always importing but also always setting module to None
                     if grass_location == all_locations_value:
                         add = True
                     else:
@@ -101,17 +102,20 @@
                                 add = True  # cases when it is explicit
                             if grass_location in locations:
                                 add = True  # standard case with given location
-                    if add:
-                        modules.append(GrassTestPythonModule(name=name,
-                                                             module=m,
-                                                             tested_dir=root,
-                                                             file_dir=full,
-                                                             abs_file_path=os.path.join(abspath, name + '.py')))
-                    # in else with some verbose we could tell about skiped test
                 except ImportError as e:
-                    raise ImportError('Cannot import module named %s in %s (%s)' % (name, full, e.message))
-                    # alternative is to create TestClass which will raise
-                    # see unittest.loader
+                    if add_failed_imports:
+                        add = True
+                    else:
+                        raise ImportError('Cannot import module named'
+                                          ' %s in %s (%s)'
+                                          % (name, full, e.message))
+                        # alternative is to create TestClass which will raise
+                        # see unittest.loader
+                if add:
+                    modules.append(GrassTestPythonModule(
+                        name=name, module=None, tested_dir=root, file_dir=full,
+                        abs_file_path=os.path.join(abspath, name + '.py')))
+                # in else with some verbose we could tell about skiped test
     return modules
 
 

Modified: grass/trunk/lib/python/gunittest/main.py
===================================================================
--- grass/trunk/lib/python/gunittest/main.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/main.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -13,6 +13,7 @@
 
 import os
 import sys
+import argparse
 
 from unittest.main import TestProgram, USAGE_AS_MAIN
 TestProgram.USAGE = USAGE_AS_MAIN
@@ -103,11 +104,6 @@
     sys.exit(not program.result.wasSuccessful())
 
 
-# TODO: test or main? test looks more general
-# unittest has main() but doctest has testmod()
-main = test
-
-
 def discovery():
     """Recursively find all tests in testsuite directories and run them
 
@@ -124,23 +120,41 @@
 
 # TODO: makefile rule should depend on the whole build
 # TODO: create a full interface (using grass parser or argparse)
-if __name__ == '__main__':
-    if len(sys.argv) == 4:
-        gisdbase = sys.argv[1]
-        location = sys.argv[2]
-        location_shortcut = sys.argv[3]
-    elif len(sys.argv) == 3:
-        location = sys.argv[1]
-        location_shortcut = sys.argv[2]
+def main():
+    parser = argparse.ArgumentParser(
+    description='Run test files in all testsuite directories starting'
+                ' from the current one'
+                ' (runs on active GRASS session)')
+    parser.add_argument('--location', dest='location', action='store',
+                        help='Name of location where to perform test', required=True)
+    parser.add_argument('--location-type', dest='location_type', action='store',
+                        default='nc',
+                        help='Type of tests which should be run'
+                             ' (tag corresponding to location)')
+    parser.add_argument('--grassdata', dest='gisdbase', action='store',
+                        default=None,
+                        help='GRASS data(base) (GISDBASE) directory'
+                        ' (current GISDBASE by default)')
+    parser.add_argument('--output', dest='output', action='store',
+                        default='testreport',
+                        help='Output directory')
+    args = parser.parse_args()
+    gisdbase = args.gisdbase
+    if gisdbase is None:
+        # here we already rely on being in GRASS session
         gisdbase = gcore.gisenv()['GISDBASE']
-    else:
-        sys.stderr.write("Usage: %s [gisdbase] location location_shortcut\n" % sys.argv[0])
+    location = args.location
+    location_type = args.location_type
+
+    if not gisdbase:
+        sys.stderr.write("GISDBASE (grassdata directory)"
+                         " cannot be empty string\n" % gisdbase)
         sys.exit(1)
-    assert gisdbase
     if not os.path.exists(gisdbase):
-        sys.stderr.write("GISDBASE <%s> does not exist\n" % gisdbase)
+        sys.stderr.write("GISDBASE (grassdata directory) <%s>"
+                         " does not exist\n" % gisdbase)
         sys.exit(1)
-    results_dir = 'testreport'
+    results_dir = args.output
     silent_rmtree(results_dir)  # TODO: too brute force?
 
     start_dir = '.'
@@ -149,9 +163,14 @@
         start_dir=start_dir,
         file_anonymizer=FileAnonymizer(paths_to_remove=[abs_start_dir]))
     # TODO: remove also results dir from files
+    # as an enhancemnt
     # we can just iterate over all locations available in database
-    # but the we don't know the right location label/shortcut
+    # but the we don't know the right location type (category, label, shortcut)
     invoker.run_in_location(gisdbase=gisdbase,
                             location=location,
-                            location_shortcut=location_shortcut,
+                            location_type=location_type,
                             results_dir=results_dir)
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())

Modified: grass/trunk/lib/python/gunittest/multireport.py
===================================================================
--- grass/trunk/lib/python/gunittest/multireport.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/multireport.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -8,6 +8,8 @@
 import operator
 from collections import defaultdict
 
+
+# TODO: we should be able to work without matplotlib
 import matplotlib
 matplotlib.use('Agg')
 import matplotlib.pyplot as plt
@@ -22,6 +24,8 @@
     def __init__(self):
         self.timestamp = None
         self.svn_revision = None
+        self.location = None
+        self.location_type = None
 
         self.total = None
         self.successes = None
@@ -142,7 +146,17 @@
             )
         for result in results:
             # TODO: include name to summary file
-            name = os.path.basename(result.report)
+            # now using location or test report directory as name
+            if result.location != 'unknown':
+                name = result.location
+            else:
+                name = os.path.basename(result.report)
+                if not name:
+                    # Python basename returns '' for 'abc/'
+                    for d in reversed(os.path.split(result.report)):
+                        if d:
+                            name = d
+                            break
             per_test = success_to_html_percent(
                 total=result.total, successes=result.successes)
             per_file = success_to_html_percent(
@@ -152,7 +166,7 @@
                 '<tr>'
                 '<td><a href={report_path}/index.html>{result.timestamp}</a></td>'
                 '<td>{result.svn_revision}</td>'
-                '<td><a href={result.report}/index.html>{name}</a></td>'
+                '<td><a href={report_path}/index.html>{name}</a></td>'
                 '<td>{pfiles}</td><td>{ptests}</td>'
                 '</tr>'
                 .format(result=result, name=name, report_path=report_path,
@@ -216,8 +230,15 @@
             result.report = report
 
             # let's consider no location as valid state and use 'unknown'
-            location = summary.get('location', 'unknown')
-            results_in_locations[location].append(result)
+            result.location = summary.get('location', 'unknown')
+            result.location_type = summary.get('location_type', 'unknown')
+            # grouping accoring to location types
+            # this can cause that two actual locations tested at the same time
+            # will end up together, this is not ideal but testing with
+            # one location type and different actual locations is not standard
+            # and although it will not break anything it will not give a nice
+            # report
+            results_in_locations[result.location_type].append(result)
 
             all_results.append(result)
             del result
@@ -236,12 +257,17 @@
         '<tbody>'
         )
 
-    for location, results in results_in_locations.iteritems():
+    for location_type, results in results_in_locations.iteritems():
         results = sorted(results, key=operator.attrgetter('timestamp'))
-        # TODO: document: location shortcut must be a valid dir name
-        directory = os.path.join(output, location)
+        # TODO: document: location type must be a valid dir name
+        directory = os.path.join(output, location_type)
         ensure_dir(directory)
 
+        if location_type == 'unknown':
+            title = 'Test reports'
+        else:
+            title = 'Test reports for ' + location_type + ' location'
+
         x = [date2num(result.timestamp) for result in results]
         xlabels = [result.timestamp.strftime("%Y-%m-%d") + ' (r' + result.svn_revision + ')' for result in results]
         tests_plot(x=x, xlabels=xlabels, results=results,
@@ -255,7 +281,8 @@
                   images=['tests_plot.png', 'files_plot.png', 'info_plot.png'],
                   captions=['Success of individual tests', 'Success of test files',
                             'Additional information'],
-                  directory=directory)
+                  directory=directory,
+                  title=title)
 
         files_successes = sum(result.files_successes for result in results)
         files_total = sum(result.files_total for result in results)
@@ -270,7 +297,7 @@
             '<td><a href={location}/index.html>{location}</a></td>'
             '<td>{pfiles}</td><td>{ptests}</td>'
             '</tr>'
-            .format(location=location,
+            .format(location=location_type,
                     pfiles=per_file, ptests=per_test))
     locations_main_page.write('</tbody></table>')
     locations_main_page.write('</body></html>')

Modified: grass/trunk/lib/python/gunittest/reporters.py
===================================================================
--- grass/trunk/lib/python/gunittest/reporters.py	2014-07-31 18:55:38 UTC (rev 61488)
+++ grass/trunk/lib/python/gunittest/reporters.py	2014-07-31 20:32:00 UTC (rev 61489)
@@ -22,8 +22,6 @@
 import types
 import re
 
-import grass.script as gscript
-
 from .utils import ensure_dir
 from .checkers import text_to_keyvalue
 
@@ -73,6 +71,7 @@
         pass
 
 
+# TODO: why not remove GISDBASE by default?
 class FileAnonymizer(object):
     def __init__(self, paths_to_remove, remove_gisbase=True,
                  remove_gisdbase=False):
@@ -81,7 +80,10 @@
             gisbase = os.environ['GISBASE']
             self._paths_to_remove.append(gisbase)
         if remove_gisdbase:
-            gisdbase = gscript.gis.get['GISDBASE']
+            # import only when really needed to avoid problems with
+            # translations when environment is not set properly
+            import grass.script as gscript
+            gisdbase = gscript.gisenv()['GISDBASE']
             self._paths_to_remove.append(gisdbase)
         if paths_to_remove:
             self._paths_to_remove.extend(paths_to_remove)
@@ -685,7 +687,7 @@
                 modules = [modules]
             file_index.write(
                 '<tr><td>Tested modules</td><td>{}</td></tr>'.format(
-                    ', '.join(modules)))
+                    ', '.join(sorted(set(modules)))))
         file_index.write('<tbody><table>')
 
         # here we would have also links to coverage, profiling, ...
@@ -727,11 +729,14 @@
             # a stream can be added and if not none, we could write
 
 
+# TODO: document info: additional information to be stored type: dict
+# allows to overwrite what was collected
 class GrassTestFilesKeyValueReporter(GrassTestFilesCountingReporter):
 
-    def __init__(self):
+    def __init__(self, info=None):
         super(GrassTestFilesKeyValueReporter, self).__init__()
         self.result_dir = None
+        self._info = info
 
     def start(self, results_dir):
         super(GrassTestFilesKeyValueReporter, self).start(results_dir)
@@ -799,6 +804,10 @@
         summary['timestamp'] = self.main_start_time.strftime('%Y-%m-%d %H:%M:%S')
         # TODO: add some general metadata here (passed in constructor)
 
+        # add additional information
+        for key, value in self._info.iteritems():
+            summary[key] = value
+
         summary_filename = os.path.join(self.result_dir,
                                         'test_keyvalue_result.txt')
         with open(summary_filename, 'w') as summary_file:



More information about the grass-commit mailing list