live-acceptance.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Read-only anonymous GET acceptance; never launches or changes the backend."""
  2. import json
  3. from collections import Counter
  4. from datetime import datetime, timezone
  5. from pathlib import Path
  6. from urllib.request import Request, urlopen
  7. from urllib.error import HTTPError, URLError
  8. URL = 'http://192.168.110.96:8301/api/cockpit/v1/infrastructure/geometry'
  9. OUTPUT = Path(__file__).resolve().parent / 'live-acceptance.json'
  10. checks = []
  11. def check(name, passed):
  12. checks.append({'name': name, 'passed': bool(passed)})
  13. report = {'url': URL, 'checkedAt': datetime.now(timezone.utc).isoformat(),
  14. 'authorizationHeaderSent': False, 'checks': checks}
  15. try:
  16. with urlopen(Request(URL, headers={'Accept': 'application/json'}), timeout=30) as response:
  17. report['httpStatus'] = response.status
  18. envelope = json.load(response)
  19. check('HTTP 200 and business code 200', report['httpStatus'] == 200 and envelope.get('code') == 200)
  20. data = envelope.get('data', {})
  21. check('GeoJSON FeatureCollection', data.get('type') == 'FeatureCollection')
  22. features = data.get('features', [])
  23. coverage = data.get('coverage', [])
  24. report['featureCount'] = len(features)
  25. report['coverage'] = coverage
  26. report['industries'] = dict(Counter(f.get('properties', {}).get('industry') for f in features))
  27. report['pointTypes'] = dict(Counter(str(f.get('properties', {}).get('pointType')) for f in features
  28. if f.get('properties', {}).get('source') == 'gas_pipe_point'))
  29. report['validationStates'] = dict(Counter(f.get('properties', {}).get('coordinateValidation', {}).get('state')
  30. for f in features))
  31. report['validationReasons'] = dict(Counter(str(f.get('properties', {}).get('coordinateValidation', {}).get('reason'))
  32. for f in features))
  33. check('Feature IDs unique', len({f.get('id') for f in features}) == len(features))
  34. check('Feature types and namespaced source IDs', all(f.get('type') == 'Feature' and
  35. f.get('id') == f.get('properties', {}).get('source', '') + ':' + str(f.get('properties', {}).get('sourceId'))
  36. for f in features))
  37. valid_shapes = True
  38. valid_validation = True
  39. geometry_consistent = True
  40. raw_consistent = True
  41. expected_industries = {'pipe_network_base': {'GAS', 'WATER_SUPPLY', 'DRAINAGE', 'UNKNOWN'},
  42. 'gas_pipe_point': {'GAS'}, 'manhole_base': {'MANHOLE'}}
  43. check('Industry source domains preserved', all(f.get('properties', {}).get('industry') in
  44. expected_industries.get(f.get('properties', {}).get('source'), set()) for f in features))
  45. check('Point type and network relationship explicitly present', all('pointType' in f.get('properties', {}) and
  46. 'networkId' in f.get('properties', {}) for f in features))
  47. for feature in features:
  48. p = feature.get('properties', {})
  49. validation = p.get('coordinateValidation', {})
  50. validations = p.get('coordinateValidations', [])
  51. raw = p.get('sourceCoordinates', [])
  52. geometry = feature.get('geometry')
  53. line = p.get('source') == 'pipe_network_base'
  54. expected = 2 if line else 1
  55. valid_validation &= all({'state', 'reason', 'renderable', 'crs'} <= set(v) and
  56. v.get('renderable') is False and v.get('crs') == 'WGS84'
  57. for v in [validation, *validations])
  58. valid_validation &= validation.get('state') in {'INVALID', 'UNVERIFIED'} and len(validations) == expected
  59. raw_consistent &= len(raw) == expected and all(isinstance(pair, list) and len(pair) == 2 for pair in raw)
  60. if validation.get('state') == 'INVALID':
  61. geometry_consistent &= 'geometry' in feature and geometry is None and bool(validation.get('reason'))
  62. else:
  63. geometry_consistent &= geometry is not None and validation.get('reason') == 'UNVERIFIED_COORDINATE'
  64. if geometry is None:
  65. continue
  66. valid_shapes &= geometry.get('type') == ('LineString' if line else 'Point')
  67. coordinates = geometry.get('coordinates')
  68. raw_consistent &= coordinates == (raw if line else raw[0])
  69. pairs = coordinates if line else [coordinates]
  70. valid_shapes &= len(pairs) == expected
  71. for pair in pairs:
  72. valid_shapes &= isinstance(pair, list) and len(pair) == 2
  73. if not isinstance(pair, list) or len(pair) != 2:
  74. continue
  75. lon, lat = pair
  76. valid_shapes &= isinstance(lon, (int, float)) and isinstance(lat, (int, float))
  77. if isinstance(lon, (int, float)) and isinstance(lat, (int, float)):
  78. valid_shapes &= -180 <= lon <= 180 and -90 <= lat <= 90 and (lon, lat) != (0, 0)
  79. check('Real two-endpoint lines and points use longitude latitude order', valid_shapes and raw_consistent)
  80. check('Coordinate validation same shape and no fabricated verified state', valid_validation)
  81. check('Invalid geometry is null and unverified geometry remains nonrenderable', geometry_consistent)
  82. expected_coverage = {('GAS', 'LineString'), ('WATER_SUPPLY', 'LineString'), ('DRAINAGE', 'LineString'),
  83. ('GAS', 'Point'), ('WATER_SUPPLY', 'Point'), ('DRAINAGE', 'Point'),
  84. ('MANHOLE', 'Point'), ('MANHOLE', 'LineString'), ('UNKNOWN', 'LineString')}
  85. check('All industry coverage entries present once', len(coverage) == 9 and
  86. {(c.get('industry'), c.get('geometryType')) for c in coverage} == expected_coverage)
  87. count_consistent = True
  88. for item in coverage:
  89. matching = [f for f in features if f.get('properties', {}).get('source') == item.get('source') and
  90. f.get('properties', {}).get('industry') == item.get('industry')]
  91. count_consistent &= item.get('recordCount') == len(matching)
  92. count_consistent &= item.get('geometryCount') == sum(f.get('geometry') is not None for f in matching)
  93. count_consistent &= item.get('renderableCount') == 0
  94. if item.get('source'):
  95. count_consistent &= item.get('state') == ('AVAILABLE' if matching else 'EMPTY')
  96. check('Coverage counts recompute from collection', count_consistent)
  97. gaps = {(c.get('industry'), c.get('geometryType')): c for c in coverage}
  98. check('Water drainage manhole coverage gaps explicit', all(
  99. gaps.get(key, {}).get('state') == 'UNAVAILABLE' and gaps.get(key, {}).get('reason') == reason
  100. for key, reason in [(('WATER_SUPPLY', 'Point'), 'NO_COMPONENT_SOURCE'),
  101. (('DRAINAGE', 'Point'), 'NO_TYPED_COMPONENT_SOURCE'),
  102. (('MANHOLE', 'LineString'), 'NO_PIPELINE_SOURCE')]))
  103. report['observedInvalidCount'] = sum(f.get('properties', {}).get('coordinateValidation', {}).get('state') == 'INVALID'
  104. for f in features)
  105. report['coverageMissingSamples'] = [source for source in expected_industries if
  106. not any(f.get('properties', {}).get('source') == source for f in features)]
  107. check('Real pipeline sample exists', any(f.get('properties', {}).get('source') == 'pipe_network_base' for f in features))
  108. except HTTPError as error:
  109. report['httpStatus'] = error.code
  110. report['error'] = error.read(4096).decode('utf-8', errors='replace')
  111. check('Endpoint available', False)
  112. except (URLError, OSError, ValueError, TypeError, KeyError) as error:
  113. report['error'] = str(error)
  114. check('Probe completed', False)
  115. report['passed'] = bool(checks) and all(c['passed'] for c in checks)
  116. OUTPUT.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
  117. print(json.dumps(report, ensure_ascii=False, indent=2))
  118. raise SystemExit(0 if report['passed'] else 1)