123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- # -*- coding: utf-8 -*-
- """!@package grass.gunittest.invoker
- @brief GRASS Python testing framework test files invoker (runner)
- Copyright (C) 2014 by the GRASS Development Team
- This program is free software under the GNU General Public
- License (>=v2). Read the file COPYING that comes with GRASS GIS
- for details.
- @author Vaclav Petras
- """
- import os
- import sys
- import shutil
- import string
- import subprocess
- from unittest.main import TestProgram, USAGE_AS_MAIN
- TestProgram.USAGE = USAGE_AS_MAIN
- from .checkers import text_to_keyvalue
- from .loader import GrassTestLoader, discover_modules
- from .reporters import (GrassTestFilesMultiReporter,
- GrassTestFilesTextReporter, GrassTestFilesHtmlReporter,
- TestsuiteDirReporter, get_svn_path_authors)
- from .utils import silent_rmtree, ensure_dir
- import grass.script.setup as gsetup
- import collections
- import types
- # TODO: change text_to_keyvalue to same sep as here
- def keyvalue_to_text(keyvalue, sep='=', vsep='\n', isep=',',
- last_vertical=None):
- if not last_vertical:
- last_vertical = vsep == '\n'
- items = []
- for key, value in keyvalue.iteritems():
- # TODO: use isep for iterables other than strings
- if (not isinstance(value, types.StringTypes)
- and isinstance(value, collections.Iterable)):
- # TODO: this does not work for list of non-strings
- value = isep.join(value)
- items.append('{key}{sep}{value}'.format(
- key=key, sep=sep, value=value))
- text = vsep.join(items)
- if last_vertical:
- text = text + vsep
- return text
- # TODO: this might be more extend then update
- def update_keyval_file(filename, module, returncode):
- if os.path.exists(filename):
- with open(filename, 'r') as keyval_file:
- keyval = text_to_keyvalue(keyval_file.read(), sep='=')
- else:
- keyval = {}
- # this is for one file
- # TODO: testing authors are more appropriate stat for testsuite dir index
- test_file_authors = get_svn_path_authors(module.abs_file_path)
- # always owerwrite name and ok
- keyval['name'] = module.name
- keyval['tested_dir'] = module.tested_dir
- if 'status' not in keyval.keys():
- 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
- class GrassTestFilesInvoker(object):
- """A class used to invoke test files and create the main report"""
- # TODO: it is not clear what clean_outputs mean, if should be split
- # std stream, random outputs, saved results, profiling
- # not stdout and stderr if they contain test results
- # we can also save only failed tests, or generate only if assert fails
- def __init__(self, start_dir,
- clean_mapsets=True, clean_outputs=True, clean_before=True,
- testsuite_dir='testsuite'):
- """
- :param bool clean_mapsets: if the mapsets should be removed
- :param bool clean_outputs: meaning is unclear: random tests outputs,
- saved images from maps, profiling?
- :param bool clean_before: if mapsets, outputs, and results
- should be removed before the tests start
- (advantageous when the previous run left everything behind)
- """
- self.start_dir = start_dir
- self.clean_mapsets = clean_mapsets
- self.clean_outputs = clean_outputs
- self.clean_before = clean_before
- self.testsuite_dir = testsuite_dir # TODO: solve distribution of this constant
- # reporter is created for each call of run_in_location()
- self.reporter = None
- self.testsuite_dirs = None
- def _create_mapset(self, gisdbase, location, module):
- """Create mapset according to informations in module.
- :param loader.GrassTestPythonModule module:
- """
- # using path.sep but also / and \ for cases when it is confused
- # (namely the case of Unix path on MS Windows)
- # replace . to get rid of unclean path
- # TODO: clean paths
- # note that backslash cannot be at the end of raw string
- dir_as_name = module.tested_dir.translate(string.maketrans(r'/\.', '___'))
- mapset = dir_as_name + '_' + module.name
- # TODO: use grass module to do this? but we are not in the right gisdbase
- mapset_dir = os.path.join(gisdbase, location, mapset)
- if self.clean_before:
- silent_rmtree(mapset_dir)
- os.mkdir(mapset_dir)
- # TODO: default region in mapset will be what?
- # copy WIND file from PERMANENT
- # TODO: this should be a function in grass.script (used also in gis_set.py, PyGRASS also has its way with Mapset)
- # TODO: are premisions an issue here?
- shutil.copy(os.path.join(gisdbase, location, 'PERMANENT', 'WIND'),
- os.path.join(mapset_dir))
- return mapset, mapset_dir
- def _run_test_module(self, module, results_dir, gisdbase, location):
- """Run one test file."""
- self.testsuite_dirs[module.tested_dir].append(module.name)
- cwd = os.path.join(results_dir, module.tested_dir, module.name)
- data_dir = os.path.join(module.file_dir, 'data')
- if os.path.exists(data_dir):
- # TODO: link dir intead of copy tree
- shutil.copytree(data_dir, os.path.join(cwd, 'data'),
- ignore=shutil.ignore_patterns('*.svn*'))
- ensure_dir(os.path.abspath(cwd))
- # TODO: put this to constructor and copy here again
- env = os.environ.copy()
- mapset, mapset_dir = self._create_mapset(gisdbase, location, module)
- gisrc = gsetup.write_gisrc(gisdbase, location, mapset)
- env['GISRC'] = gisrc
- stdout_path = os.path.join(cwd, 'stdout.txt')
- stderr_path = os.path.join(cwd, 'stderr.txt')
- stdout = open(stdout_path, 'w')
- stderr = open(stderr_path, 'w')
- self.reporter.start_file_test(module)
- # TODO: we might clean the directory here before test if non-empty
- # TODO: use some grass function to run?
- # add also '-Qwarn'?
- p = subprocess.Popen([sys.executable, '-tt', '-3',
- module.abs_file_path],
- cwd=cwd, env=env,
- stdout=stdout, stderr=stderr)
- returncode = p.wait()
- stdout.close()
- stderr.close()
- test_summary = update_keyval_file(
- os.path.join(cwd, 'test_keyvalue_result.txt'),
- module=module, returncode=returncode)
- self.reporter.end_file_test(module=module, cwd=cwd,
- returncode=returncode,
- stdout=stdout_path, stderr=stderr_path,
- test_summary=test_summary)
- # TODO: add some try-except or with for better error handling
- os.remove(gisrc)
- # TODO: only if clean up
- if self.clean_mapsets:
- shutil.rmtree(mapset_dir)
- def run_in_location(self, gisdbase, location, location_shortcut,
- results_dir):
- """Run tests in a given location"""
- if os.path.abspath(results_dir) == os.path.abspath(self.start_dir):
- raise RuntimeError("Results root directory should not be the same"
- " as discovery start directory")
- self.reporter = GrassTestFilesMultiReporter(
- reporters=[
- GrassTestFilesTextReporter(stream=sys.stderr),
- GrassTestFilesHtmlReporter(),
- ])
- 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,
- file_pattern=GrassTestLoader.files_in_testsuite,
- skip_dirs=GrassTestLoader.skip_dirs,
- testsuite_dir=GrassTestLoader.testsuite_dir,
- all_locations_value=GrassTestLoader.all_tests_value,
- universal_location_value=GrassTestLoader.universal_tests_value,
- import_modules=False)
- self.reporter.start(results_dir)
- for module in modules:
- self._run_test_module(module=module, results_dir=results_dir,
- gisdbase=gisdbase, location=location)
- testsuite_dir_reporter = TestsuiteDirReporter(
- main_page_name='testsuites.html')
- testsuite_dir_reporter.report_for_dirs(root=results_dir,
- directories=self.testsuite_dirs)
- self.reporter.finish()
|