aboutsummaryrefslogtreecommitdiff
path: root/test/gigs/test_json.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/gigs/test_json.py')
-rw-r--r--test/gigs/test_json.py528
1 files changed, 0 insertions, 528 deletions
diff --git a/test/gigs/test_json.py b/test/gigs/test_json.py
deleted file mode 100644
index 11aec31e..00000000
--- a/test/gigs/test_json.py
+++ /dev/null
@@ -1,528 +0,0 @@
-# This is a framework to test GIGS, Geospatial Integrity of geoscience
-# software. Which is published by International Association of Oil & Gas
-# Producers as a resources for how geospatial software should give consistent
-# and expected results.
-#
-# This could be expanded to be used with other testing frameworks.
-#
-# - See more at: http://www.iogp.org/Geomatics#2521115-gigs
-#
-
-#
-# == REQUIREMENTS ==
-# - Python 2.7 or 3.3+
-# - pyproj (optional but highly recommended) - read TESTNOTES.md
-
-# == TODO list ==
-# * Python 3 was not running all the test cases seemingly due to how it uses
-# an iterable version of zip. -- FIXED
-# * driver for proj4js (javascript)
-# - could be written using nodejs using subprocess
-# - could use PyExecJS (currently not advanced enough) or PyV8
-# * call cs2cs directly
-# - WORKING but 2 orders of magnitude slower than pyproj, and
-# potentially insecure shelling?
-
-
-import argparse
-import glob
-import json
-import logging
-import os
-import platform
-import sys
-import subprocess
-from tempfile import NamedTemporaryFile
-
-# only for debug
-# import pdb
-
-try:
- import pyproj
-except ImportError as e_pyproj:
- pass
-
-
-PY_MAJOR = sys.version_info[0]
-PY2 = (PY_MAJOR == 2)
-
-# Python 2/3 Compatibility code ########################################
-if PY_MAJOR >= 3:
- # list-producing versions of the major Python iterating functions
- # lzip acts like Python 2.x zip function.
- # taken from Python-Future http://python-future.org/
- # future.utils.lzip()
-
- def lzip(*args, **kwargs):
- return list(zip(*args, **kwargs))
-
-else:
- from __builtin__ import zip as lzip
-########################################################################
-
-
-def list_count_matches(coords, ex_coords, tolerance):
- """
- counts coordinates in lists that match and don't match.
- assumes that lists are the same length (they should be)
-
- coords - coordinates
- ex_cords - expected cooridnate
- tolerance - difference allowed between the coordinates
-
- returns tuple (matches, non_matches)
- """
- matches, non_matches = 0, 0
- iter_ex_coords = iter(ex_coords)
- for c in coords:
- ex_coord = next(iter_ex_coords)
- if match_func(c, ex_coord, tolerance):
- matches = matches + 1
- else:
- non_matches = non_matches + 1
-
- return (matches, non_matches)
-
-
-def match_func(cor, exc, tolerance):
- """
- Check if coordinate matches expected coordinate within a given tolerance.
-
- cor - coordinate
- exc - expected coordinate
- tolerance - error rate
- float coordinate elements will be checked based on this value
- list/tuple coordinate elements will be checked based on the
- corresponding values
- return bool
- """
- if len(exc) == 3:
- # coordinate triples
- coord_diff = abs(cor[0] - exc[0]), abs(cor[1] - exc[1]), abs(cor[2] - exc[2])
- if isinstance(tolerance, float):
- matching = coord_diff < (tolerance, tolerance, tolerance)
- elif isinstance(tolerance, (list, tuple)): # FIXME is list's length atleast 3?
- matching = coord_diff < tuple(tolerance)
- else:
- # assume coordinate pairs
- coord_diff = abs(cor[0] - exc[0]), abs(cor[1] - exc[1])
- if isinstance(tolerance, float):
- matching = coord_diff < (tolerance, tolerance)
- elif isinstance(tolerance, (list, tuple)): # FIXME is list's length atleast 2?
- matching = coord_diff < tuple(tolerance)
-
- if matching is False:
- logging.info('non-match, calculated coordinate: {c1}\n'
- 'expected coordinate: {c2}\n difference:{res}\n'
- 'tolerance: {tol}\n'
- ''.format(c1=cor, c2=exc, res=coord_diff, tol=tolerance))
-
- return matching
-
-
-# parse multiple tests and call TransformTest
-# TODO: needs some awareness of the driver, so driver_info function in
-# TransformTest classes can be called, could allow a dummy instance of
-# Driver and move all the initization code to another function? Or allow
-# dipatch function to check if everything is in order do a transform.
-# Not an elegant solution.
-class TransformRunner(object):
- def __init__(self, fn_pattern, driver, **kwargs):
- """
- fn_pattern - file name or file name pattern (example "*.json")
- driver - this is the type of driver to run
- kwargs - parameters passed to the respective driver TransformTest class
- """
- self.driver = driver
- json_input = []
- if os.path.isfile(fn_pattern):
- with open(fn_pattern, 'rt') as fp:
- logging.info("Reading json from file '{0}'".format(fn_pattern))
- json_dict = json.load(fp, parse_int=float)
- json_dict['filename'] = fn_pattern
- json_input = [json_dict]
- # is this a glob/fnmatch style pattern?
- elif '*' in fn_pattern or '?' in fn_pattern:
- filename_iter = glob.iglob(fn_pattern)
- for filename in filename_iter:
- with open(filename, 'rt') as fp:
- logging.info("Reading json from file '{0}'".format(filename))
- j_input = json.load(fp, parse_int=float)
- if isinstance(j_input, dict):
- j_input['filename'] = filename
- json_input.append(j_input)
- elif isinstance(j_input, list):
- # FIXME could build a new list with the filename dict
- logging.warning("json file is a list, not quite supported - FIXME")
- json_input.extend(j_input)
- else:
- raise ValueError('json input in an unknown type')
- else:
- raise TypeError('filename_pattern must be a valid filename or pattern')
-
- self.runs = json_input
- self.kwargs = kwargs
-
- def dispatch(self):
- """
- main loop to run all the tests
- """
- total_matches, total_no_matches, success_code = 0, 0, 0
- for run in self.runs:
- if self.driver == 'pyproj':
- trantst = TransformTestPyProj(run, self.kwargs)
- elif self.driver == 'cs2cs':
- trantst = TransformTestCs2cs(run, self.kwargs)
- else:
- raise ValueError("driver {0} is not a valid test driver".format(self.driver))
- matches, no_matches = trantst.dispatch()
- total_matches += matches
- total_no_matches += no_matches
- success_code += no_matches
-
- return total_matches, total_no_matches, success_code
-
-
-# parses and runs a single GIGS test case
-class TransformTestBase(object):
- """
- TransformTest common code for testing framework.
- """
- def __init__(self, json_dict, kwargs):
- """
- json_dict must dictonary from json
- """
- if not isinstance(json_dict, dict): # must be a json dictionary
- raise TypeError("json_source must be a dictionary type not {0}"
- "".format(type(json_dict)))
-
- # require keys 'coordinates' and 'projections'
- if 'coordinates' not in json_dict:
- raise KeyError("TransformTest.__init__ requires 'coordinates' key"
- " in json source input")
-
- if 'projections' not in json_dict:
- raise KeyError("TransformTest.__init__ requires 'projections' key"
- " in json source input")
-
- logging.info('Number of coordinate pairs to test: {0}'.format(
- len(json_dict['coordinates'])))
-
- self.run_test_args = kwargs.get('test')
-
- # unpack coordinates
- self.coords_left, self.coords_right = lzip(*json_dict['coordinates'])
-
- self.testobj = json_dict
-
- def runner_conversion(self, **kwargs):
- """
- tests a single conversion
-
- return tuple (num_matches, num_no_matches)
- """
-
- # get tolerance, if not set tolerance to a precise value
- tolerances = kwargs.get('tolerances', [0.0000000000001, 0.0000000000001])
-
- test_right = self.transform(self.proj_left, self.proj_right, self.coords_left)
- test_left = self.transform(self.proj_right, self.proj_left, self.coords_right)
-
- results1 = list_count_matches(test_right, self.coords_right, tolerances[1])
- results2 = list_count_matches(test_left, self.coords_left, tolerances[0])
-
- return (results1[0] + results2[0], results1[1] + results2[1])
-
- def runner_roundtrip(self, **kwargs):
- """
- rountrip test using pyproj.
-
- times - number roundtrips to perform
- tolerance - TODO explain the structure of why this is a list
-
- return tuple (num_matches, num_no_matches)
- """
- times = None
-
- # get variables
- times = int(kwargs.get('times'))
- tolerances = kwargs.get('tolerances', [0.0000000000001, 0.0000000000001])
-
- # keep the transformations separate, so as to not cross contaminate the
- # results.
-
- # process roundtrip for the left coordinates - Test 1
- test1_left = self.coords_left
- for i in range(times):
- test1_right = self.transform(self.proj_left, self.proj_right, test1_left)
- test1_left = self.transform(self.proj_right, self.proj_left, test1_right)
-
- # process roundtrip for the right coordinates - Test 2
- test2_right = self.coords_right
- for i in range(times):
- test2_left = self.transform(self.proj_right, self.proj_left, test2_right)
- test2_right = self.transform(self.proj_left, self.proj_right, test2_left)
-
- results = (
- list_count_matches(test1_right, self.coords_right, tolerances[1]),
- list_count_matches(test1_left, self.coords_left, tolerances[0]),
- list_count_matches(test2_right, self.coords_right, tolerances[1]),
- list_count_matches(test2_left, self.coords_left, tolerances[0])
- )
-
- return tuple(sum(x) for x in lzip(*results))
-
- # TODO: Untested. Not useful for GIGS.
- def runner_onedirection(self, **kwargs):
- """
- Perform a conversion in only one direction, not both. Useful for
- testing convergence of a coordinate system.
-
- return tuple (num_matches, num_no_matches)
- """
- # get variables
- direction = kwargs.get('direction')
- # get tolerance, if not set tolerance to a precise value
- tolerances = kwargs.get('tolerances', [0.0000000000001, 0.0000000000001])
-
- if direction not in ('left-to-right', 'right-to-left'):
- raise ValueError('direction must be left-to-right or right-to-left, not: {0}'.format(direction))
-
- if direction == 'left-to-right':
- test_dest_right = self.transform(self.proj_left, self.proj_right, self.coords_left)
- results = list_count_matches(test_dest_right, self.coords_right, tolerances[1])
- elif direction == 'right-to-left':
- test_dest_left = self.transform(self.proj_right, self.proj_left, self.coords_right)
- results = list_count_matches(test_dest_left, self.coords_left, tolerances[0])
- else:
- raise RuntimeError('Unexpected state of value direction "{0}" in runner_onedirection'.format(direction))
-
- return results
-
- # placeholder function
- def transform(self, src_crs, dst_crs, coords):
- pass
-
- def dispatch(self):
- """
- main
- """
- matches, no_matches = 0, 0
-
- # convert to tuple
- run_tests = self.run_test_args,
- if self.run_test_args is None:
- run_tests = ('conversion', 'roundtrip')
-
- logging.info('Testing: {0}'.format(self.testobj['description']))
-
- for test in self.testobj['tests']:
- m_res, nm_res = None, None
- if test['type'] not in run_tests:
- # skip test
- continue
-
- if test['type'] == 'conversion':
- m_res, nm_res = self.runner_conversion(**test)
- elif test['type'] == 'roundtrip':
- m_res, nm_res = self.runner_roundtrip(**test)
-
- if nm_res == 0:
- logging.info(" {0}... All {1} match.".format(test['type'], m_res))
- else:
- logging.info(" {0}... matches: {1} doesn't match: {2}"
- "".format(test['type'], m_res, nm_res))
-
- matches += m_res
- no_matches += nm_res
-
- return matches, no_matches
-
- # placeholder function -- TODO How should this be exposed?
- def driver_info(self):
- return "base class"
-
-
-class TransformTestPyProj(TransformTestBase):
- """
- TransformTest uses pyproj to run tests.
- """
- def __init__(self, json_dict, kwargs):
- # call super class
- TransformTestBase.__init__(self, json_dict, kwargs)
-
- # setup projections
- try:
- self.proj_left = pyproj.Proj(json_dict['projections'][0], preserve_units=True)
- except RuntimeError as e:
- logging.error('pyproj raised a RuntimeError for projection string:'
- ' "{0}"'.format(json_dict['projections'][0]))
- raise RuntimeError(e)
- try:
- self.proj_right = pyproj.Proj(json_dict['projections'][1], preserve_units=True)
- except RuntimeError as e:
- logging.error('pyproj raised a RuntimeError for projection string:'
- ' "{0}"'.format(json_dict['projections'][1]))
- raise RuntimeError(e)
-
- def transform(self, src_crs, dst_crs, coords):
- return self.pyproj_transform_ex(src_crs, dst_crs, coords)
-
- def driver_info(self):
- return 'pyproj {0}\nproj4 {1}\n'.format(
- pyproj.__version__, self.proj4_version())
-
- # TODO: currently dead code, unneeded for the pyproj repo. version
- # as of 2016-05-24.
- def proj4_version():
- """
- Gives the proj.4 library's version number. (requires pyproj)
- returns string, so proj.4 version 4.9.3 will return "4.9.3"
- """
- try:
- return pyproj.proj_version_str
- except AttributeError:
- # for pyproj versions 1.9.5.1 and before, this will run
- # Get PROJ4 version in a floating point number
- proj4_ver_num = pyproj.Proj(proj='latlong').proj_version
-
- # convert float into a version string (4.90 becomes '4.9.0')
- return '.'.join(str(int(proj4_ver_num * 100)))
-
- def pyproj_transform_ex(self, proj_src, proj_dst, coords):
- """
- wrapper for pyproj.transform to do all the zipping of the coordinates
-
- returns coordinate list
- """
- # are these coordinate triples?
- if len(coords[0]) == 3:
- xi, yi, zi = lzip(*coords)
- xo, yo, zo = pyproj.transform(proj_src, proj_dst, xi, yi, zi)
- return lzip(xo, yo, zo)
-
- # assume list of coordinate pairs
- xi, yi = lzip(*coords)
- xo, yo = pyproj.transform(proj_src, proj_dst, xi, yi)
- return lzip(xo, yo)
-
-
-class TransformTestCs2cs(TransformTestBase):
- def __init__(self, json_dict, kwargs):
- # call super class
- TransformTestBase.__init__(self, json_dict, kwargs)
-
- # copy proj4 projection strings
- self.proj_left, self.proj_right = json_dict['projections']
-
- self.exe = kwargs.get('exe', 'cs2cs')
-
- # when the exe is not the default, check if the file exists
- if self.exe == 'cs2cs' or not os.path.isfile(self.exe):
- raise RuntimeError('cannot find cs2cs executable file: {}'
- ''.format(self.exe))
-
- def transform(self, src_crs, dst_crs, coords):
- # send points to a temporary file
- # TODO Should this use with statement?
- tmpfn = NamedTemporaryFile(mode='w+t', delete=False)
- for point in coords:
- # convert list of float values into a list of strings
- point = [str(e) for e in point]
- # print('POINT: {}'.format(point))
- tmpfn.write(' '.join(point) + '\n')
-
- tmpfn.flush()
- command = "{exe} {proj_from} +to {proj_to} -f %.13f {filename}".format(
- exe=self.exe, proj_from=src_crs, proj_to=dst_crs, filename=tmpfn.name)
- tmpfn.close()
-
- logging.debug('Running Popen on command "{0}"'.format(command))
-
- if platform.system() == 'Windows':
- shell = False
- else:
- # shell=True according to the subprocess documentation has some
- # security implications
- # Linux seems to need this
- shell = True
-
- # call cs2cs
- outs = subprocess.check_output(command, shell=shell)
-
- # delete temporary filename
- os.unlink(tmpfn.name)
-
- # print('RESULTS OUTS: {}\n'.format(outs))
- # print('RESULTS ERRS: {}\n'.format(errs))
-
- # outs
- # print('RESULTS LINE: {}\n'.format([line.split() for line in outs]))
- coords = []
-
- # process output
- for line in outs.splitlines():
- # print("LINE: {}".format(line))
- coord = []
- for e in line.split():
- coord.append(float(e))
-
- coords.append(coord)
-
- # print('COORDS: {}\n'.format(coords))
- return coords
-
- def driver_info(self):
- shell = True # see transform() for info.
- if platform.system() == 'Windows':
- shell = False
-
- outs = subprocess.check_output(self.exe, shell=shell)
- return 'PROJ.4 version: ' + outs.splitlines()[0] + '\n'
-
-
-if __name__ == '__main__':
- # logging.basicConfig(level=logging.DEBUG)
- logging.basicConfig(level=logging.INFO)
-
- parser = argparse.ArgumentParser(description='Test PROJ.4 using a JSON file.')
- parser.add_argument('-e', '--exe',
- help="executable with path default: 'cs2cs' (needed for cs2cs driver)")
-
- parser.add_argument('-d', '--driver', default='pyproj',
- help='driver to test')
-
- parser.add_argument('-t', '--test',
- help='only run these test types (valid values: conversion or roundtrip)')
-
- # get json file names and/or glob patterns
- parser.add_argument('testnames_pat_json', nargs=argparse.REMAINDER,
- help='single filename or glob wildcard patern')
-
- args = parser.parse_args()
-
- # test that the arguments have sensible values
- if args.driver not in ('cs2cs', 'pyproj'):
- raise ValueError('driver "{}" is not a valid driver'.format(args.driver))
-
- logging.info('Python {}'.format(sys.version))
- logging.info('using driver: {}'.format(args.driver))
-
- # there could be a version command for the TransformRunner TODO
-
- match_results, nonmatch_results, success_code = 0, 0, 0
- for test_name in args.testnames_pat_json:
- tratst = TransformRunner(test_name, driver=args.driver, exe=args.exe,
- test=args.test)
- m_res, nm_res, success_cd = tratst.dispatch()
- match_results += m_res
- nonmatch_results += nm_res
- success_code += success_cd
-
- logging.info("----------------------------------------")
- logging.info("TOTAL: matches: {0} non-matching: {1}"
- "".format(match_results, nonmatch_results))
-
- # exit status code is the number of non-matching results
- # This should play nicely with Travis and similar testing.
- sys.exit(success_code)