test_production_coverage.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. from __future__ import annotations
  2. import unittest
  3. from production_build_agents.contracts.models import (
  4. ExpectationEvaluation,
  5. RequirementEvaluation,
  6. )
  7. from production_build_agents.contracts.production_assembly_evaluation import (
  8. evaluate_production_assembly_delivery,
  9. )
  10. from production_build_agents.contracts.production_coverage import (
  11. build_production_coverage,
  12. segment_artifact_ref_key,
  13. )
  14. from tests.support.production_fixtures import (
  15. ProductionScenario,
  16. immutable_ref,
  17. )
  18. class ProductionCoverageContractTest(unittest.TestCase):
  19. def setUp(self) -> None:
  20. self.scenario = ProductionScenario()
  21. self.__dict__.update(vars(self.scenario))
  22. def test_missing_segment_package_returns_contract_issue(self) -> None:
  23. codes = {
  24. item.code
  25. for item in evaluate_production_assembly_delivery(
  26. self.planning,
  27. self.plan,
  28. self.global_delivery,
  29. self.production_package,
  30. self.assembly,
  31. {"Segment1": self.package_map["Segment1"]},
  32. {"Segment1": self.delivery_map["Segment1"]},
  33. self.assembly_delivery,
  34. production_package_ref=immutable_ref("production-package"),
  35. assembly_package_ref=immutable_ref("assembly-package"),
  36. segment_package_refs={
  37. "Segment1": self.segment_package_refs["Segment1"]
  38. },
  39. )
  40. }
  41. self.assertIn("assembly_segment_package_set_mismatch", codes)
  42. self.assertIn("production_coverage_missing_segment_package", codes)
  43. def test_unconsumed_satisfied_global_requirement_does_not_fail(
  44. self,
  45. ) -> None:
  46. omitted = self.global_delivery.model_copy(
  47. update={
  48. "requirement_evaluations": [
  49. *self.global_delivery.requirement_evaluations,
  50. RequirementEvaluation(
  51. requirement_id="Requirement3",
  52. expectation_evaluations=[
  53. ExpectationEvaluation(
  54. expectation_id=(
  55. "Requirement3-Expectation1"
  56. ),
  57. expected_count=1,
  58. actual_count=1,
  59. artifact_ids=["Task3-v1-artifact-1"],
  60. satisfied=True,
  61. )
  62. ],
  63. ),
  64. ]
  65. }
  66. )
  67. coverage = build_production_coverage(
  68. self.assembly,
  69. self.package_map,
  70. self.delivery_map,
  71. {
  72. segment_artifact_ref_key(artifact)
  73. for item in self.assembly.accepted_segments
  74. for artifact in (
  75. item.primary_artifact,
  76. item.subtitle_artifact,
  77. )
  78. },
  79. omitted,
  80. )
  81. self.assertTrue(coverage.satisfied)
  82. self.assertNotIn(
  83. "Requirement3-Expectation1",
  84. coverage.covered_expectation_ids,
  85. )
  86. def test_unsatisfied_global_requirement_cannot_vacuously_pass(
  87. self,
  88. ) -> None:
  89. unsatisfied = self.global_delivery.model_copy(
  90. update={
  91. "requirement_evaluations": [
  92. *self.global_delivery.requirement_evaluations,
  93. RequirementEvaluation(
  94. requirement_id="Requirement3",
  95. expectation_evaluations=[
  96. ExpectationEvaluation(
  97. expectation_id=(
  98. "Requirement3-Expectation1"
  99. ),
  100. expected_count=1,
  101. actual_count=0,
  102. artifact_ids=[],
  103. satisfied=False,
  104. )
  105. ],
  106. ),
  107. ]
  108. }
  109. )
  110. coverage = build_production_coverage(
  111. self.assembly,
  112. self.package_map,
  113. self.delivery_map,
  114. {
  115. segment_artifact_ref_key(artifact)
  116. for item in self.assembly.accepted_segments
  117. for artifact in (
  118. item.primary_artifact,
  119. item.subtitle_artifact,
  120. )
  121. },
  122. unsatisfied,
  123. )
  124. self.assertFalse(coverage.satisfied)
  125. self.assertNotIn(
  126. "Requirement3",
  127. coverage.covered_requirement_ids,
  128. )
  129. self.assertIn(
  130. "Requirement3-Expectation1",
  131. coverage.required_expectation_ids,
  132. )
  133. def test_planned_but_unused_artifact_is_not_reported_covered(
  134. self,
  135. ) -> None:
  136. package = self.package_map["Segment1"]
  137. delivery = self.delivery_map["Segment1"]
  138. source_id = package.artifact_inputs[0].artifact_id
  139. anchor_id = package.shared_visual_anchor.anchor_id
  140. lineages = [
  141. lineage.model_copy(
  142. update={
  143. "input_artifact_ids": [
  144. anchor_id if item == source_id else item
  145. for item in lineage.input_artifact_ids
  146. ]
  147. }
  148. )
  149. for lineage in delivery.artifact_graph.lineages
  150. ]
  151. changed = delivery.model_copy(
  152. update={
  153. "artifact_graph": delivery.artifact_graph.model_copy(
  154. update={"lineages": lineages}
  155. )
  156. }
  157. )
  158. coverage = build_production_coverage(
  159. self.assembly,
  160. self.package_map,
  161. {**self.delivery_map, "Segment1": changed},
  162. {
  163. segment_artifact_ref_key(artifact)
  164. for item in self.assembly.accepted_segments
  165. for artifact in (
  166. item.primary_artifact,
  167. item.subtitle_artifact,
  168. )
  169. },
  170. self.global_delivery,
  171. )
  172. segment = coverage.segment_results[0]
  173. self.assertIn(source_id, segment.required_global_artifact_ids)
  174. self.assertNotIn(source_id, segment.covered_global_artifact_ids)
  175. self.assertTrue(segment.satisfied)
  176. self.assertTrue(coverage.satisfied)
  177. def test_unplanned_but_actually_used_artifact_is_reported_covered(
  178. self,
  179. ) -> None:
  180. package = self.package_map["Segment1"]
  181. delivery = self.delivery_map["Segment1"]
  182. planned_id = package.artifact_inputs[0].artifact_id
  183. extra_id = "Task3-v1-artifact-1"
  184. extra_artifact = self.global_delivery.active_artifacts[0].model_copy(
  185. update={
  186. "artifact_id": extra_id,
  187. "uri": "/runs/global/artifacts/task-3.json",
  188. "content_sha256": "3" * 64,
  189. "description": "未由 Planner 选择但实际使用的素材",
  190. }
  191. )
  192. changed_global = self.global_delivery.model_copy(
  193. update={
  194. "active_artifacts": [
  195. *self.global_delivery.active_artifacts,
  196. extra_artifact,
  197. ],
  198. "requirement_evaluations": [
  199. *self.global_delivery.requirement_evaluations,
  200. RequirementEvaluation(
  201. requirement_id="Requirement3",
  202. expectation_evaluations=[
  203. ExpectationEvaluation(
  204. expectation_id=(
  205. "Requirement3-Expectation1"
  206. ),
  207. expected_count=1,
  208. actual_count=1,
  209. artifact_ids=[extra_id],
  210. satisfied=True,
  211. )
  212. ],
  213. actual_artifact_ids=[extra_id],
  214. ),
  215. ],
  216. }
  217. )
  218. lineages = [
  219. lineage.model_copy(
  220. update={
  221. "input_artifact_ids": [
  222. extra_id if item == planned_id else item
  223. for item in lineage.input_artifact_ids
  224. ]
  225. }
  226. )
  227. for lineage in delivery.artifact_graph.lineages
  228. ]
  229. changed_delivery = delivery.model_copy(
  230. update={
  231. "artifact_graph": delivery.artifact_graph.model_copy(
  232. update={"lineages": lineages}
  233. )
  234. }
  235. )
  236. coverage = build_production_coverage(
  237. self.assembly,
  238. self.package_map,
  239. {**self.delivery_map, "Segment1": changed_delivery},
  240. {
  241. segment_artifact_ref_key(artifact)
  242. for item in self.assembly.accepted_segments
  243. for artifact in (
  244. item.primary_artifact,
  245. item.subtitle_artifact,
  246. )
  247. },
  248. changed_global,
  249. )
  250. segment = coverage.segment_results[0]
  251. self.assertIn(planned_id, segment.required_global_artifact_ids)
  252. self.assertNotIn(planned_id, segment.covered_global_artifact_ids)
  253. self.assertIn(extra_id, segment.covered_global_artifact_ids)
  254. self.assertIn(
  255. "Requirement3-Expectation1",
  256. segment.covered_expectation_ids,
  257. )
  258. self.assertTrue(coverage.satisfied)
  259. def test_missing_production_input_still_fails_coverage(self) -> None:
  260. delivery = self.delivery_map["Segment1"]
  261. changed_delivery = delivery.model_copy(
  262. update={
  263. "artifact_graph": delivery.artifact_graph.model_copy(
  264. update={
  265. "lineages": [
  266. lineage.model_copy(
  267. update={"production_input_ids": []}
  268. )
  269. for lineage in (
  270. delivery.artifact_graph.lineages
  271. )
  272. ]
  273. }
  274. )
  275. }
  276. )
  277. coverage = build_production_coverage(
  278. self.assembly,
  279. self.package_map,
  280. {**self.delivery_map, "Segment1": changed_delivery},
  281. {
  282. segment_artifact_ref_key(artifact)
  283. for item in self.assembly.accepted_segments
  284. for artifact in (
  285. item.primary_artifact,
  286. item.subtitle_artifact,
  287. )
  288. },
  289. self.global_delivery,
  290. )
  291. segment = coverage.segment_results[0]
  292. self.assertEqual(segment.covered_production_input_ids, [])
  293. self.assertFalse(segment.satisfied)
  294. self.assertIn("Segment1", coverage.reachable_segment_ids)
  295. self.assertFalse(coverage.satisfied)
  296. def test_unassigned_package_input_is_not_required_coverage(self) -> None:
  297. package = self.package_map["Segment1"]
  298. unassigned = package.production_inputs[0].model_copy(
  299. update={
  300. "input_id": "ProductionInput-" + "f" * 64,
  301. "source_path": "$.制作表.关系结果[0]",
  302. "content_sha256": "e" * 64,
  303. "value": {"关系": "由整片而非当前镜头验收"},
  304. }
  305. )
  306. changed_package = package.model_copy(
  307. update={
  308. "production_inputs": [
  309. *package.production_inputs,
  310. unassigned,
  311. ]
  312. }
  313. )
  314. coverage = build_production_coverage(
  315. self.assembly,
  316. {**self.package_map, "Segment1": changed_package},
  317. self.delivery_map,
  318. {
  319. segment_artifact_ref_key(artifact)
  320. for item in self.assembly.accepted_segments
  321. for artifact in (
  322. item.primary_artifact,
  323. item.subtitle_artifact,
  324. )
  325. },
  326. self.global_delivery,
  327. )
  328. segment = coverage.segment_results[0]
  329. self.assertEqual(
  330. segment.required_production_input_ids,
  331. package.shots[0].production_input_ids,
  332. )
  333. self.assertNotIn(
  334. unassigned.input_id,
  335. segment.covered_production_input_ids,
  336. )
  337. self.assertTrue(segment.satisfied)
  338. self.assertTrue(coverage.satisfied)
  339. if __name__ == "__main__":
  340. import unittest
  341. unittest.main()