test_landing_video_dedupe.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import sys
  2. import unittest
  3. from pathlib import Path
  4. from unittest.mock import patch
  5. _HERE = Path(__file__).parent
  6. sys.path.insert(0, str(_HERE))
  7. import execute_creation_once # noqa: E402
  8. class _Strategy:
  9. def __init__(self, material_source):
  10. self.material_source = material_source
  11. self.use_ai_generated = material_source == "ai_generated"
  12. self.ai_fallback_to_history = False
  13. class LandingVideoDedupeTest(unittest.TestCase):
  14. def test_same_crowd_package_excludes_landing_video_across_accounts_in_one_run(self):
  15. calls = []
  16. def fake_prepare(account_id, adgroup_id, *, excluded_material_ids, excluded_landing_ids, **kwargs):
  17. calls.append((account_id, adgroup_id, set(excluded_landing_ids)))
  18. return {
  19. "account_id": account_id,
  20. "adgroup_id": adgroup_id,
  21. "audience_tier": "wx*商业",
  22. "landing_video_id": 1000 + account_id,
  23. "_material_id": f"m-{account_id}",
  24. "material_source": "history",
  25. }
  26. with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1, 2]), \
  27. patch.object(execute_creation_once, "load_excluded_ad_ids_from_adjustment", return_value=set()), \
  28. patch.object(execute_creation_once, "get_account_crowd_package", return_value="wx*商业"), \
  29. patch.object(execute_creation_once, "load_recent_landing_usage_counts", return_value={"history": {}, "ai_generated": {}}), \
  30. patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("history")), \
  31. patch.object(execute_creation_once, "find_ads_needing_creatives", side_effect=[
  32. [{"adgroup_id": 101, "creative_count": 3}],
  33. [{"adgroup_id": 201, "creative_count": 3}],
  34. ]), \
  35. patch.object(execute_creation_once, "build_landing_candidate_pool", return_value=object()), \
  36. patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
  37. patch.object(execute_creation_once, "record_prepared_material_usage"):
  38. records = execute_creation_once.phase1_prepare(target_creatives=4)
  39. self.assertEqual(2, len(records))
  40. self.assertEqual(set(), calls[0][2])
  41. self.assertEqual({1001}, calls[1][2])
  42. def test_ai_landing_dedupe_is_independent_from_history_and_limits_once_per_ai_pool(self):
  43. calls = []
  44. def fake_prepare(account_id, adgroup_id, *, excluded_material_ids, excluded_landing_ids, **kwargs):
  45. calls.append(set(excluded_landing_ids))
  46. return {
  47. "account_id": account_id,
  48. "adgroup_id": adgroup_id,
  49. "audience_tier": "wx*商业",
  50. "landing_video_id": 777,
  51. "_material_id": f"ai:{account_id}-{adgroup_id}-{len(calls)}",
  52. "material_source": "ai_generated",
  53. }
  54. with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1, 2]), \
  55. patch.object(execute_creation_once, "load_excluded_ad_ids_from_adjustment", return_value=set()), \
  56. patch.object(execute_creation_once, "get_account_crowd_package", return_value="wx*商业"), \
  57. patch.object(
  58. execute_creation_once,
  59. "load_recent_landing_usage_counts",
  60. return_value={"history": {777: 1}, "ai_generated": {}},
  61. ), \
  62. patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("ai_generated")), \
  63. patch.object(execute_creation_once, "find_ads_needing_creatives", side_effect=[
  64. [{"adgroup_id": 101, "creative_count": 11}],
  65. [{"adgroup_id": 201, "creative_count": 11}],
  66. ]), \
  67. patch.object(execute_creation_once, "build_landing_candidate_pool", return_value=object()), \
  68. patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
  69. patch.object(execute_creation_once, "record_prepared_material_usage"):
  70. records = execute_creation_once.phase1_prepare(target_creatives=12)
  71. self.assertEqual(1, len(records))
  72. self.assertEqual(set(), calls[0])
  73. self.assertEqual({777}, calls[1])
  74. def test_phase1_reuses_landing_candidate_pool_for_multiple_creative_attempts(self):
  75. pool = object()
  76. build_calls = []
  77. prepare_calls = []
  78. def fake_build_pool(account_id):
  79. build_calls.append(account_id)
  80. return pool
  81. def fake_prepare(
  82. account_id,
  83. adgroup_id,
  84. *,
  85. excluded_material_ids,
  86. excluded_landing_ids,
  87. landing_candidates,
  88. failed_landing_ids,
  89. ):
  90. prepare_calls.append((landing_candidates, failed_landing_ids))
  91. return {
  92. "account_id": account_id,
  93. "adgroup_id": adgroup_id,
  94. "audience_tier": "R330",
  95. "landing_video_id": 1000 + len(prepare_calls),
  96. "_material_id": f"ai:{len(prepare_calls)}",
  97. "material_source": "ai_generated",
  98. }
  99. with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1]), \
  100. patch.object(execute_creation_once, "load_excluded_ad_ids_from_adjustment", return_value=set()), \
  101. patch.object(execute_creation_once, "get_account_crowd_package", return_value="R330"), \
  102. patch.object(execute_creation_once, "load_recent_landing_usage_counts", return_value={"history": {}, "ai_generated": {}}), \
  103. patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("ai_generated")), \
  104. patch.object(execute_creation_once, "find_ads_needing_creatives", return_value=[
  105. {"adgroup_id": 101, "creative_count": 0},
  106. ]), \
  107. patch.object(execute_creation_once, "build_landing_candidate_pool", side_effect=fake_build_pool), \
  108. patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
  109. patch.object(execute_creation_once, "record_prepared_material_usage"):
  110. records = execute_creation_once.phase1_prepare(target_creatives=2)
  111. self.assertEqual(2, len(records))
  112. self.assertEqual([1], build_calls)
  113. self.assertEqual([(pool, set()), (pool, set())], prepare_calls)
  114. def test_phase1_carries_failed_landing_ids_between_attempts(self):
  115. calls = []
  116. def fake_prepare(
  117. account_id,
  118. adgroup_id,
  119. *,
  120. excluded_material_ids,
  121. excluded_landing_ids,
  122. landing_candidates,
  123. failed_landing_ids,
  124. ):
  125. calls.append(set(failed_landing_ids))
  126. if len(calls) == 1:
  127. failed_landing_ids.add(777)
  128. return None
  129. return {
  130. "account_id": account_id,
  131. "adgroup_id": adgroup_id,
  132. "audience_tier": "R330",
  133. "landing_video_id": 888,
  134. "_material_id": "ai:ok",
  135. "material_source": "ai_generated",
  136. }
  137. with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1]), \
  138. patch.object(execute_creation_once, "load_excluded_ad_ids_from_adjustment", return_value=set()), \
  139. patch.object(execute_creation_once, "get_account_crowd_package", return_value="R330"), \
  140. patch.object(execute_creation_once, "load_recent_landing_usage_counts", return_value={"history": {}, "ai_generated": {}}), \
  141. patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("ai_generated")), \
  142. patch.object(execute_creation_once, "find_ads_needing_creatives", return_value=[
  143. {"adgroup_id": 101, "creative_count": 0},
  144. ]), \
  145. patch.object(execute_creation_once, "build_landing_candidate_pool", return_value=object()), \
  146. patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
  147. patch.object(execute_creation_once, "record_prepared_material_usage"):
  148. records = execute_creation_once.phase1_prepare(target_creatives=2)
  149. self.assertEqual(1, len(records))
  150. self.assertEqual([set(), {777}], calls)
  151. if __name__ == "__main__":
  152. unittest.main()