def test_conditionally_required_invalid(self): """Validation: verify conditional validity behavior when invalid.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, }, "string_a": { "name": "The first parameter", "about": "About the first parameter", "type": "option_string", "required": "number_a", "validation_options": { "options": ['AAA', 'BBB'] } } } args = {'string_a': "ZZZ", "number_a": 1} self.assertEqual( [(['string_a'], "Value must be one of: ['AAA', 'BBB']")], validation.validate(args, spec))
def test_validation_exception(self): """Validation: Verify error when an unexpected exception occurs.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, }, } args = {'number_a': 1} try: # Patch in a new function that raises an exception into the # validation functions dictionary. patched_function = Mock(side_effect=ValueError('foo')) validation._VALIDATION_FUNCS['number'] = patched_function validation_warnings = validation.validate(args, spec) finally: # No matter what happens with this test, always restore the state # of the validation functions dict. validation._VALIDATION_FUNCS['number'] = (validation.check_number) self.assertEqual( validation_warnings, [(['number_a'], 'An unexpected error occurred in validation')])
def test_conditional_requirement_missing_var(self): """Validation: check AssertionError if expression is missing a var.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, }, "number_b": { "name": "The second parameter", "about": "About the second parameter", "type": "number", "required": False, }, "number_c": { "name": "The third parameter", "about": "About the third parameter", "type": "number", "required": "some_var_not_in_args", } } args = { "number_a": 123, "number_b": 456, } with self.assertRaises(AssertionError) as cm: validation_warnings = validation.validate(args, spec) self.assertTrue('some_var_not_in_args' in str(cm.exception))
def test_allow_extra_keys(self): """Including extra keys in args that aren't in ARGS_SPEC should work""" from natcap.invest import validation args = {'a': 'a', 'b': 'b'} spec = { 'a': { 'type': 'freestyle_string', 'name': 'a', 'about': 'a freestyle string', 'required': True } } message = 'DEBUG:natcap.invest.validation:Provided key b does not exist in ARGS_SPEC' with self.assertLogs('natcap.invest.validation', level='DEBUG') as cm: validation.validate(args, spec) self.assertTrue(message in cm.output)
def test_slow_to_open(self): """Test timeout by mocking a CSV that is slow to open""" from natcap.invest import validation # make an actual file so that `check_file` will pass path = os.path.join(self.workspace_dir, 'slow.csv') with open(path, 'w') as file: file.write('1,2,3') spec = { "mock_csv_path": { "type": "csv", "required": True, "about": "A CSV that will be mocked.", "name": "CSV" } } # validate a mocked CSV that will take 6 seconds to return a value args = {"mock_csv_path": path} # define a side effect for the mock that will sleep # for longer than the allowed timeout def delay(*args, **kwargs): time.sleep(6) return [] # make a copy of the real _VALIDATION_FUNCS and override the CSV function mock_validation_funcs = { key: val for key, val in validation._VALIDATION_FUNCS.items() } mock_validation_funcs['csv'] = functools.partial( validation.timeout, delay) # replace the validation.check_csv with the mock function, and try to validate with unittest.mock.patch('natcap.invest.validation._VALIDATION_FUNCS', mock_validation_funcs): with warnings.catch_warnings(record=True) as ws: # cause all warnings to always be triggered warnings.simplefilter("always") validation.validate(args, spec) self.assertTrue(len(ws) == 1) self.assertTrue('timed out' in str(ws[0].message))
def test_requirement_no_value(self): """Validation: verify absolute requirement without value.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, } } args = {'number_a': ''} self.assertEqual( [(['number_a'], 'Input is required but has no value')], validation.validate(args, spec)) args = {'number_a': None} self.assertEqual( [(['number_a'], 'Input is required but has no value')], validation.validate(args, spec))
def test_validation_other(self): """Validation: verify no error when 'other' type.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "other", "required": True, }, } args = {'number_a': 1} self.assertEqual([], validation.validate(args, spec))
def test_requirement_missing(self): """Validation: verify absolute requirement on missing key.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, } } args = {} self.assertEqual([(['number_a'], 'Key is missing from the args dict')], validation.validate(args, spec))
def test_invalid_value(self): """Validation: verify invalidity.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, } } args = {'number_a': 'not a number'} self.assertEqual([(['number_a'], ("Value 'not a number' could not be interpreted " "as a number"))], validation.validate(args, spec))
def test_conditional_requirement_not_required(self): """Validation: unrequired conditional requirement should always pass""" from natcap.invest import validation csv_a_path = os.path.join(self.workspace_dir, 'csv_a.csv') csv_b_path = os.path.join(self.workspace_dir, 'csv_b.csv') # initialize test CSV files with open(csv_a_path, 'w') as csv: csv.write('a,b,c') with open(csv_b_path, 'w') as csv: csv.write('1,2,3') spec = { "condition": { "name": "A condition that determines requirements", "about": "About the condition", "type": "boolean", "required": False, }, "csv_a": { "name": "Conditionally required CSV A", "about": "About CSV A", "type": "csv", "required": "condition", }, "csv_b": { "name": "Conditonally required CSV B", "about": "About CSV B", "type": "csv", "required": "not condition", } } # because condition = True, it shouldn't matter that the # csv_b parameter wouldn't pass validation args = { "condition": True, "csv_a": csv_a_path, "csv_b": 'x' + csv_b_path # introduce a typo } validation_warnings = validation.validate(args, spec) self.assertEqual(validation_warnings, [])
def test_spatial_overlap_error_undefined_projection(self): """Validation: check spatial overlap message when no projection""" from natcap.invest import validation spec = { 'raster_a': { 'type': 'raster', 'name': 'raster 1', 'about': 'raster 1', 'required': True, }, 'raster_b': { 'type': 'raster', 'name': 'raster 2', 'about': 'raster 2', 'required': True, } } driver = gdal.GetDriverByName('GTiff') filepath_1 = os.path.join(self.workspace_dir, 'raster_1.tif') filepath_2 = os.path.join(self.workspace_dir, 'raster_2.tif') raster_1 = driver.Create(filepath_1, 3, 3, 1, gdal.GDT_Int32) wgs84_srs = osr.SpatialReference() wgs84_srs.ImportFromEPSG(4326) raster_1.SetProjection(wgs84_srs.ExportToWkt()) raster_1.SetGeoTransform([1, 1, 0, 1, 0, 1]) raster_1 = None # don't define a projection for the second raster driver.Create(filepath_2, 3, 3, 1, gdal.GDT_Int32) args = {'raster_a': filepath_1, 'raster_b': filepath_2} validation_warnings = validation.validate( args, spec, { 'spatial_keys': list(args.keys()), 'different_projections_ok': True }) expected = [(['raster_b'], 'Dataset must have a valid projection.')] self.assertEqual(validation_warnings, expected)
def test_conditional_validity_recursive(self): """Validation: check that we can require from nested conditions.""" from natcap.invest import validation spec = {} previous_key = None args = {} for letter in string.ascii_uppercase[:10]: key = 'arg_%s' % letter spec[key] = { 'name': 'name ' + key, 'about': 'about ' + key, 'type': 'freestyle_string', 'required': previous_key } previous_key = key args[key] = key del args[previous_key] # delete the last addition to the dict. self.assertEqual([(['arg_J'], 'Key is missing from the args dict')], validation.validate(args, spec))
def test_conditionally_required_no_value(self): """Validation: verify conditional requirement when no value.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, }, "string_a": { "name": "The first parameter", "about": "About the first parameter", "type": "freestyle_string", "required": "number_a", } } args = {'string_a': None, "number_a": 1} self.assertEqual([(['string_a'], 'Key is required but has no value')], validation.validate(args, spec))
def test_conditional_requirement(self): """Validation: check that conditional requirements works.""" from natcap.invest import validation spec = { "number_a": { "name": "The first parameter", "about": "About the first parameter", "type": "number", "required": True, }, "number_b": { "name": "The second parameter", "about": "About the second parameter", "type": "number", "required": False, }, "number_c": { "name": "The third parameter", "about": "About the third parameter", "type": "number", "required": "number_b", }, "number_d": { "name": "The fourth parameter", "about": "About the fourth parameter", "type": "number", "required": "number_b | number_c", }, "number_e": { "name": "The fifth parameter", "about": "About the fifth parameter", "type": "number", "required": "number_b & number_d" }, "number_f": { "name": "The sixth parameter", "about": "About the sixth parameter", "type": "number", "required": "not number_b" } } args = { "number_a": 123, "number_b": 456, } validation_warnings = validation.validate(args, spec) self.assertEqual(sorted(validation_warnings), [ (['number_c'], 'Key is missing from the args dict'), (['number_d'], 'Key is missing from the args dict'), ]) args = { "number_a": 123, "number_b": 456, "number_c": 1, "number_d": 3, "number_e": 4, } self.assertEqual([], validation.validate(args, spec)) args = { "number_a": 123, } validation_warnings = validation.validate(args, spec) self.assertEqual(sorted(validation_warnings), [(['number_f'], 'Key is missing from the args dict')])
def validate(args, limit_to=None): return validation.validate(args, args_spec)
def test_spatial_overlap_error(self): """Validation: check that we return an error on spatial mismatch.""" from natcap.invest import validation spec = { 'raster_a': { 'type': 'raster', 'name': 'raster 1', 'about': 'raster 1', 'required': True, }, 'raster_b': { 'type': 'raster', 'name': 'raster 2', 'about': 'raster 2', 'required': True, }, 'vector_a': { 'type': 'vector', 'name': 'vector 1', 'about': 'vector 1', 'required': True, } } driver = gdal.GetDriverByName('GTiff') filepath_1 = os.path.join(self.workspace_dir, 'raster_1.tif') filepath_2 = os.path.join(self.workspace_dir, 'raster_2.tif') reference_filepath = os.path.join(self.workspace_dir, 'reference.gpkg') # Filepaths 1 and 2 are obviously outside of UTM zone 31N. for filepath, geotransform, epsg_code in ((filepath_1, [ 1, 1, 0, 1, 0, 1 ], 4326), (filepath_2, [100, 1, 0, 100, 0, 1], 4326)): raster = driver.Create(filepath, 3, 3, 1, gdal.GDT_Int32) wgs84_srs = osr.SpatialReference() wgs84_srs.ImportFromEPSG(epsg_code) raster.SetProjection(wgs84_srs.ExportToWkt()) raster.SetGeoTransform(geotransform) raster = None gpkg_driver = gdal.GetDriverByName('GPKG') vector = gpkg_driver.Create(reference_filepath, 0, 0, 0, gdal.GDT_Unknown) vector_srs = osr.SpatialReference() vector_srs.ImportFromEPSG(32731) # UTM 31N layer = vector.CreateLayer('layer', vector_srs, ogr.wkbPoint) new_feature = ogr.Feature(layer.GetLayerDefn()) new_feature.SetGeometry(ogr.CreateGeometryFromWkt('POINT 1 1')) new_feature = None layer = None vector = None args = { 'raster_a': filepath_1, 'raster_b': filepath_2, 'vector_a': reference_filepath, } validation_warnings = validation.validate( args, spec, { 'spatial_keys': list(args.keys()), 'different_projections_ok': True }) self.assertEqual(len(validation_warnings), 1) self.assertEqual(set(args.keys()), set(validation_warnings[0][0])) self.assertTrue( 'Bounding boxes do not intersect' in validation_warnings[0][1])
def test_spatial_overlap_error_optional_args(self): """Validation: check for spatial mismatch with insufficient args.""" from natcap.invest import validation spec = { 'raster_a': { 'type': 'raster', 'name': 'raster 1', 'about': 'raster 1', 'required': True, }, 'raster_b': { 'type': 'raster', 'name': 'raster 2', 'about': 'raster 2', 'required': False, }, 'vector_a': { 'type': 'vector', 'name': 'vector 1', 'about': 'vector 1', 'required': False, } } driver = gdal.GetDriverByName('GTiff') filepath_1 = os.path.join(self.workspace_dir, 'raster_1.tif') filepath_2 = os.path.join(self.workspace_dir, 'raster_2.tif') # Filepaths 1 and 2 do not overlap for filepath, geotransform, epsg_code in ((filepath_1, [ 1, 1, 0, 1, 0, 1 ], 4326), (filepath_2, [100, 1, 0, 100, 0, 1], 4326)): raster = driver.Create(filepath, 3, 3, 1, gdal.GDT_Int32) wgs84_srs = osr.SpatialReference() wgs84_srs.ImportFromEPSG(epsg_code) raster.SetProjection(wgs84_srs.ExportToWkt()) raster.SetGeoTransform(geotransform) raster = None args = { 'raster_a': filepath_1, } # There should not be a spatial overlap check at all # when less than 2 of the spatial keys are sufficient. validation_warnings = validation.validate( args, spec, { 'spatial_keys': list(spec.keys()), 'different_projections_ok': True }) self.assertEqual(len(validation_warnings), 0) # And even though there are three spatial keys in the spec, # Only the ones checked should appear in the validation output args = { 'raster_a': filepath_1, 'raster_b': filepath_2, } validation_warnings = validation.validate( args, spec, { 'spatial_keys': list(spec.keys()), 'different_projections_ok': True }) self.assertEqual(len(validation_warnings), 1) self.assertTrue( 'Bounding boxes do not intersect' in validation_warnings[0][1]) self.assertEqual(set(args.keys()), set(validation_warnings[0][0]))