test_data_rule_control_plane.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. from __future__ import annotations
  2. import copy
  3. import os
  4. import pytest
  5. from sqlalchemy import create_engine, text
  6. from sqlalchemy.orm import Session
  7. from app.core.data_rules.schema_resolver import SchemaResolver
  8. from tests.core.data_rules.test_contracts import (
  9. valid_dataflow_spec,
  10. valid_rule_spec,
  11. valid_standard_spec,
  12. )
  13. pytestmark = pytest.mark.integration
  14. class FakeMetadataCatalog:
  15. def load_schema(self, schema_ref):
  16. return {
  17. "source_revision": "integration:1",
  18. "fields": [
  19. {
  20. "name": "customer_id",
  21. "type": "string",
  22. "nullable": False,
  23. }
  24. ],
  25. }
  26. @pytest.fixture()
  27. def database_url():
  28. value = os.environ.get("TEST_DATABASE_URL")
  29. if not value:
  30. pytest.skip("TEST_DATABASE_URL is not configured")
  31. return value
  32. def test_postgres_rule_standard_and_production_line_release_is_atomic(
  33. database_url,
  34. ):
  35. from app.core.data_rules.release import ProductionLineReleaseService
  36. from app.core.data_rules.repository import DataRuleRepository
  37. engine = create_engine(database_url)
  38. with engine.connect() as connection:
  39. transaction = connection.begin()
  40. try:
  41. actor = connection.execute(
  42. text(
  43. "SELECT id::text FROM public.users "
  44. "WHERE status = 'active' ORDER BY created_at LIMIT 1"
  45. )
  46. ).scalar_one()
  47. session = Session(bind=connection)
  48. repository = DataRuleRepository(session)
  49. quality_spec = valid_rule_spec()
  50. quality_spec["steps"] = [copy.deepcopy(quality_spec["steps"][1])]
  51. quality_created = repository.create_rule_version(
  52. rule_spec=quality_spec,
  53. source_text="手机号必须为11位数字",
  54. category="standard_clause",
  55. created_by=actor,
  56. )
  57. quality = repository.publish_rule_version(
  58. version_id=quality_created["id"],
  59. published_by=actor,
  60. )
  61. transform_spec = valid_rule_spec()
  62. transform_created = repository.create_rule_version(
  63. rule_spec=transform_spec,
  64. source_text="清洗姓名并校验手机号",
  65. category="flow_scoped",
  66. created_by=actor,
  67. )
  68. transform = repository.publish_rule_version(
  69. version_id=transform_created["id"],
  70. published_by=actor,
  71. )
  72. standard_spec = valid_standard_spec(quality["id"])
  73. standard_created = repository.create_standard_version(
  74. standard_spec=standard_spec,
  75. source_text="客户手机号遵循统一格式",
  76. created_by=actor,
  77. )
  78. standard = repository.publish_standard_version(
  79. version_id=standard_created["id"],
  80. published_by=actor,
  81. )
  82. flow = valid_dataflow_spec(standard["id"], transform["id"])
  83. released = ProductionLineReleaseService(
  84. repository,
  85. schema_resolver=SchemaResolver(FakeMetadataCatalog(), repository),
  86. ).release(
  87. dataflow_uid=flow["dataflow_uid"],
  88. dataflow_spec=flow,
  89. source_text="清洗客户数据并执行客户数据标准",
  90. created_by=actor,
  91. )
  92. assert released["status"] == "released"
  93. assert released["package"]["standard_version_ids"] == [standard["id"]]
  94. assert released["package"]["rule_version_ids"] == sorted(
  95. [quality["id"], transform["id"]]
  96. )
  97. counts = connection.execute(
  98. text(
  99. "SELECT "
  100. "(SELECT COUNT(*) FROM public.dataflow_component_bindings "
  101. " WHERE dataflow_version_id = CAST(:id AS uuid)) AS bindings, "
  102. "(SELECT COUNT(*) FROM public.rule_execution_plans p "
  103. " JOIN public.dataflow_component_bindings b "
  104. " ON b.id = p.component_binding_id "
  105. " WHERE b.dataflow_version_id = CAST(:id AS uuid) "
  106. " AND p.status = 'published') AS plans"
  107. ),
  108. {"id": released["id"]},
  109. ).one()
  110. assert counts.bindings == 2
  111. assert counts.plans == 2
  112. finally:
  113. transaction.rollback()
  114. engine.dispose()