verify-unified-parity.py 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. """Compare the actual old library and new PostgreSQL binding before cutover."""
  2. from contextlib import closing
  3. import json
  4. import os
  5. from pathlib import Path
  6. import sys
  7. import argparse
  8. sys.dont_write_bytecode = True
  9. sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'server/voc-library'))
  10. import board_library as board
  11. import industry_monitor as industry
  12. import unified_store as store
  13. import voc_db
  14. import retrieval
  15. def main():
  16. p = argparse.ArgumentParser(description=__doc__)
  17. p.add_argument('--config', required=True)
  18. s = p.add_mutually_exclusive_group(required=True)
  19. s.add_argument('--registry'); s.add_argument('--db')
  20. p.add_argument('--monitoring-store', required=True)
  21. p.add_argument('--policy-usage-review');p.add_argument('--policy-usage-sha256')
  22. p.add_argument('--out', required=True)
  23. a = p.parse_args()
  24. old_env = os.environ.pop('KSF_UNIFIED_DB_CONFIG', None)
  25. original = {'registry': a.registry, 'db': a.db, 'monitoringStore': a.monitoring_store,
  26. 'policyUsageReview': a.policy_usage_review, 'policyUsageSha256': a.policy_usage_sha256}
  27. new = {**original, 'unifiedConfig': a.config}
  28. checks = []
  29. try:
  30. summary = board.run(original, {'op': 'summary'})
  31. requests = [{'op': 'summary'}, {'op': 'version'}]
  32. for entity in board.LABELS:
  33. for quality in ('analysis', 'audit'):
  34. requests.append({'op': 'browse', 'entity': entity, 'quality': quality, 'limit': 3})
  35. r = board.run(original, {'op': 'browse', 'entity': entity, 'quality': 'audit', 'limit': 1})
  36. if r['records']:
  37. requests.append({'op': 'detail', 'entity': entity, 'id': r['records'][0]['record_id']})
  38. for sort in ('reviews_asc', 'reviews_desc', 'price_asc', 'price_desc'):
  39. requests.append({'op': 'browse', 'entity': 'products', 'sort': sort, 'page': 2, 'limit': 24})
  40. for entity in ('products', 'reviews'):
  41. requests.append({'op': 'browse', 'entity': entity, 'keyword': '味道' if entity == 'reviews' else '拌面',
  42. 'filters': {'brand': 'ksf', 'category': 'banmian'}})
  43. for req in requests:
  44. before = board.run(original, req)
  45. after = board.run(new, req)
  46. store.need(before == after, 'LIBRARY_PARITY_FAILED')
  47. checks.append({'channel': 'library', 'op': req['op'], 'entity': req.get('entity'), 'equal': True})
  48. # The Python industry path also reads monitoring via the unified binding.
  49. for op in ('contract', 'overview', 'stores', 'listings', 'reviews', 'policies', 'rankings', 'plan', 'insights', 'workshop'):
  50. req = {'op': op}
  51. before = industry.run(original, req)
  52. os.environ['KSF_UNIFIED_DB_CONFIG'] = a.config
  53. try:
  54. after = industry.run(new, req)
  55. finally:
  56. os.environ.pop('KSF_UNIFIED_DB_CONFIG', None)
  57. store.need(before == after, 'INDUSTRY_PARITY_FAILED')
  58. checks.append({'channel': 'industry', 'op': op, 'equal': True})
  59. # Exact original source payloads, not merely the browser's redacted citation.
  60. db, _ = board.active_database(original)
  61. with voc_db.readonly(db) as source, store.read_research(store.research_reference(a.config)) as target:
  62. ids = [r[0] for r in source.execute('SELECT id FROM source_records ORDER BY id LIMIT 10')]
  63. for rid in ids:
  64. store.need(voc_db.get_source(source, rid) == voc_db.get_source(target, rid), 'SOURCE_PARITY_FAILED')
  65. result = {'ok': True, 'generation': summary['generation'], 'checks': checks, 'originalSourcesCompared': len(ids),
  66. 'readOnly': True, 'databaseWrites': 0, 'businessRowsChanged': 0}
  67. out = Path(a.out);out.parent.mkdir(parents=True, exist_ok=True)
  68. with out.open('x', encoding='utf-8') as f:
  69. f.write(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
  70. out.chmod(0o600)
  71. print(json.dumps({'ok': True, 'checks': len(checks), 'originalSourcesCompared': len(ids), 'receipt': str(out)}))
  72. return 0
  73. except Exception as e:
  74. code = str(e) if isinstance(e, store.StoreError) else 'PARITY_FAILED'
  75. print(json.dumps({'ok': False, 'error': {'code': code}, 'readOnly': True, 'databaseWrites': 0}))
  76. return 1
  77. finally:
  78. if old_env is not None:
  79. os.environ['KSF_UNIFIED_DB_CONFIG'] = old_env
  80. if __name__ == '__main__':
  81. sys.exit(main())