diff --git a/tests/test_validate_network.py b/tests/test_validate_network.py new file mode 100644 index 0000000..95a9362 --- /dev/null +++ b/tests/test_validate_network.py @@ -0,0 +1,119 @@ +"""Synthetic fixtures: no company INP or network data is embedded in tests.""" +import hashlib +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +spec=importlib.util.spec_from_file_location('validator',Path(__file__).resolve().parents[1]/'tools/validate_network.py') +validator=importlib.util.module_from_spec(spec) +spec.loader.exec_module(validator) + +INP='''[OPTIONS] +FLOW_UNITS CMS +LINK_OFFSETS DEPTH +[JUNCTIONS] +A 10 3 0 0 0 +B 9 4 0 0 0 +[CONDUITS] +P A B 5 0.015 0.2 0.1 0 0 +[XSECTIONS] +P CIRCULAR 0.6 0 0 0 1 +[COORDINATES] +A 0 0 +B 3 4 +''' + + +def fixture(text=INP): + return {'meta':{'sha256':hashlib.sha256(text.encode()).hexdigest(),'flowUnits':'CMS', + 'offsetMode':'DEPTH','physicalConversion':1}, + 'nodes':[{'id':'A','type':'junction','line':5,'raw':'A 10 3 0 0 0'.split(), + 'invert':10,'maxDepth':3,'top':13,'xy':[0,0],'connections':['P']}, + {'id':'B','type':'junction','line':6,'raw':'B 9 4 0 0 0'.split(), + 'invert':9,'maxDepth':4,'top':13,'xy':[3,4],'connections':['P']}], + 'links':[{'id':'P','type':'conduit','line':8,'raw':'P A B 5 0.015 0.2 0.1 0 0'.split(), + 'fromNode':'A','toNode':'B','vertices':[],'pathXY':[[0,0],[3,4]], + 'planDistance':5,'length':5,'roughness':.015,'inletOffset':.2,'outletOffset':.1, + 'invertStart':10.2,'invertEnd':9.1,'centerStart':10.5,'centerEnd':9.4, + 'lengthDelta':0,'lengthDifferenceRatio':0,'slopeInput':.22,'slopePlan':.22, + 'renderable':True,'section':{'shape':'CIRCULAR','height':.6,'width':.6, + 'diameter':.6,'barrels':1,'raw':'P CIRCULAR 0.6 0 0 0 1'.split()}}], + 'summary':{'counts':{'junction':2,'outfall':0,'storage':0,'conduit':1,'pump':0,'weir':0}, + 'coordinateCount':2,'shapes':{'CIRCULAR':1},'renderedConduits':1,'vertexCount':0},'issues':[]} + + +class ValidationTests(unittest.TestCase): + def run_case(self,data=None,text=INP): + with tempfile.TemporaryDirectory() as folder: + root=Path(folder) + (root/'source.inp').write_bytes(text.encode()) + (root/'network.json').write_text(json.dumps(data or fixture(text)),encoding='utf8') + return validator.validate(root/'source.inp',root/'network.json') + + def test_matching_fixture(self): + self.assertEqual(self.run_case()['status'],'passed') + + def test_changed_diameter_is_caught(self): + data=fixture();data['links'][0]['section']['diameter']=.9 + result=self.run_case(data) + self.assertEqual(result['status'],'failed') + self.assertIn('section.diameter',[e['field'] for e in result['mismatches']]) + + def test_changed_connection_is_caught(self): + data=fixture();data['links'][0]['toNode']='A' + self.assertEqual(self.run_case(data)['status'],'failed') + + def test_missing_facility_is_caught(self): + data=fixture();data['nodes'].pop() + self.assertEqual(self.run_case(data)['status'],'failed') + + def test_null_is_not_zero(self): + data=fixture();data['nodes'][0]['invert']=None + self.assertEqual(self.run_case(data)['status'],'failed') + + def test_duplicate_inp_id_is_caught(self): + text=INP+'A 5 6\n' + self.assertEqual(self.run_case(fixture(text),text)['status'],'failed') + + def test_source_hash_is_checked(self): + data=fixture();data['meta']['sha256']='wrong' + self.assertEqual(self.run_case(data)['status'],'failed') + + def test_elevation_offsets_are_absolute(self): + text=INP.replace('LINK_OFFSETS DEPTH','LINK_OFFSETS ELEVATION') + data=fixture(text);data['meta']['offsetMode']='ELEVATION' + data['links'][0].update(invertStart=.2,invertEnd=.1,centerStart=.5,centerEnd=.4) + data['links'][0].update(slopeInput=.02,slopePlan=.02) + self.assertEqual(self.run_case(data,text)['status'],'passed') + + def test_rectangular_width_and_height_are_distinct(self): + text=INP.replace('P CIRCULAR 0.6 0 0 0 1','P RECT_CLOSED 0.6 1.2 0 0 1') + data=fixture(text) + data['links'][0]['section'].update(shape='RECT_CLOSED',width=1.2,diameter=None, + raw='P RECT_CLOSED 0.6 1.2 0 0 1'.split()) + data['summary']['shapes']={'RECT_CLOSED':1} + self.assertEqual(self.run_case(data,text)['status'],'passed') + data['links'][0]['section']['width']=.6 + self.assertEqual(self.run_case(data,text)['status'],'failed') + + def test_missing_coordinate_requires_documented_exclusion(self): + text=INP.replace('B 3 4\n','') + data=fixture(text);data['nodes'][1]['xy']=None + data['links'][0].update(pathXY=None,planDistance=None,renderable=False, + lengthDelta=None,lengthDifferenceRatio=None,slopePlan=None) + data['summary']['coordinateCount']=1 + data['summary']['renderedConduits']=0 + self.assertEqual(self.run_case(data,text)['status'],'failed') + data['issues']=[{'code':'missing_coordinate','ids':['B']},{'code':'unplaced_link','ids':['P']}] + self.assertEqual(self.run_case(data,text)['status'],'passed') + + def test_unsupported_shape_does_not_pass(self): + text=INP.replace('CIRCULAR','CUSTOM') + with self.assertRaisesRegex(ValueError,'unsupported conduit shape'): + self.run_case(fixture(text),text) + + +if __name__=='__main__': + unittest.main()