quapp-common 0.0.14.dev5__tar.gz → 0.0.14.dev6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {quapp_common-0.0.14.dev5/quapp_common.egg-info → quapp_common-0.0.14.dev6}/PKG-INFO +1 -1
  2. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/pyproject.toml +1 -1
  3. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/async_tasks/post_processing_task.py +14 -9
  4. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/config/log_context.py +9 -1
  5. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/device/custom_device.py +23 -15
  6. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6/quapp_common.egg-info}/PKG-INFO +1 -1
  7. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common.egg-info/SOURCES.txt +1 -0
  8. quapp_common-0.0.14.dev6/tests/test_phase_coverage.py +196 -0
  9. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/LICENSE +0 -0
  10. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/README.md +0 -0
  11. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/__init__.py +0 -0
  12. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/async_tasks/__init__.py +0 -0
  13. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/async_tasks/async_invocation_task.py +0 -0
  14. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/async_tasks/async_task.py +0 -0
  15. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  16. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/__init__.py +0 -0
  17. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/backend/__init__.py +0 -0
  18. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/backend/invocation.py +0 -0
  19. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/backend/job_fetcher.py +0 -0
  20. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/backend/job_fetching.py +0 -0
  21. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/backend/job_heartbeat.py +0 -0
  22. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/backend/job_manager.py +0 -0
  23. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/bridge.py +0 -0
  24. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/callback/__init__.py +0 -0
  25. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/callback/update_job_metadata.py +0 -0
  26. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/circuit_adapter.py +0 -0
  27. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/device/__init__.py +0 -0
  28. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/device/device_selection.py +0 -0
  29. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/dispatcher.py +0 -0
  30. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/component/result_serializer.py +0 -0
  31. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/config/__init__.py +0 -0
  32. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/config/logging_config.py +0 -0
  33. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/config/thread_config.py +0 -0
  34. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/__init__.py +0 -0
  35. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/async_task/__init__.py +0 -0
  36. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  37. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  38. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  39. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/backend/__init__.py +0 -0
  40. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/backend/backend_information.py +0 -0
  41. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/callback/__init__.py +0 -0
  42. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/callback/callback_url.py +0 -0
  43. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/device/__init__.py +0 -0
  44. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/device/circuit_running_option.py +0 -0
  45. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/promise/__init__.py +0 -0
  46. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/promise/post_processing_promise.py +0 -0
  47. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/promise/promise.py +0 -0
  48. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/request/__init__.py +0 -0
  49. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/request/invocation_request.py +0 -0
  50. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/request/job_fetching_request.py +0 -0
  51. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/request/request.py +0 -0
  52. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/response/__init__.py +0 -0
  53. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/response/authentication.py +0 -0
  54. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/response/custom_header.py +0 -0
  55. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/response/error_report.py +0 -0
  56. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/data/response/job_response.py +0 -0
  57. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/__init__.py +0 -0
  58. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/base_enum.py +0 -0
  59. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/error_category.py +0 -0
  60. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/http_header.py +0 -0
  61. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/invocation_step.py +0 -0
  62. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/language.py +0 -0
  63. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/media_type.py +0 -0
  64. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/processing_unit.py +0 -0
  65. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/provider_tag.py +0 -0
  66. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/runtime_phase.py +0 -0
  67. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/sdk.py +0 -0
  68. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/status/__init__.py +0 -0
  69. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/status/job_status.py +0 -0
  70. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/status/status_code.py +0 -0
  71. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/enum/token_type.py +0 -0
  72. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/factory/__init__.py +0 -0
  73. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/factory/device_factory.py +0 -0
  74. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/factory/handler_factory.py +0 -0
  75. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/factory/provider_factory.py +0 -0
  76. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/handler/__init__.py +0 -0
  77. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/handler/handler.py +0 -0
  78. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/__init__.py +0 -0
  79. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/device/__init__.py +0 -0
  80. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/device/device.py +0 -0
  81. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/invocation.py +0 -0
  82. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/provider/__init__.py +0 -0
  83. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/model/provider/provider.py +0 -0
  84. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/__init__.py +0 -0
  85. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/error_classifier.py +0 -0
  86. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/file_utils.py +0 -0
  87. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/http_utils.py +0 -0
  88. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/invocation_failure.py +0 -0
  89. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/json_parser_utils.py +0 -0
  90. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common/util/response_utils.py +0 -0
  91. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common.egg-info/dependency_links.txt +0 -0
  92. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common.egg-info/requires.txt +0 -0
  93. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/quapp_common.egg-info/top_level.txt +0 -0
  94. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/setup.cfg +0 -0
  95. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_circuit_adapter.py +0 -0
  96. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_dispatcher_runner_error.py +0 -0
  97. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_error_classifier.py +0 -0
  98. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_error_log_dedupe.py +0 -0
  99. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_error_report.py +0 -0
  100. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_invocation_failure.py +0 -0
  101. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_job_heartbeat.py +0 -0
  102. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_json_parser_utils.py +0 -0
  103. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_language_dispatcher.py +0 -0
  104. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_log_secret_leak.py +0 -0
  105. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_logging_config.py +0 -0
  106. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_no_sdk_dependency.py +0 -0
  107. {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev6}/tests/test_result_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.14.dev5
3
+ Version: 0.0.14.dev6
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.14.dev5"
7
+ version = "0.0.14.dev6"
8
8
  description = "Quapp common library supporting Quapp Platform for Quantum Computing"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
@@ -4,6 +4,7 @@
4
4
  from .async_task import AsyncTask
5
5
  from ..config.logging_config import job_logger
6
6
  from ..data.promise.post_processing_promise import PostProcessingPromise
7
+ from ..config.log_context import phase
7
8
  from ..enum.media_type import MediaType
8
9
  from ..enum.runtime_phase import RuntimePhase
9
10
  from ..util.http_utils import get_job_id_from_url
@@ -32,15 +33,19 @@ class PostProcessingTask(AsyncTask):
32
33
  job_response.content_type = MediaType.APPLICATION_JSON
33
34
 
34
35
  try:
35
- self.logger.info("Execute post_processing ...")
36
- job_result_post_processing = self.post_processing_fn(
37
- self.promise.job_result)
38
-
39
- # print() cũ ghi thẳng ra stdout, không qua sink nên không được
40
- # redact và không có job_id.
41
- self.logger.debug(
42
- f"Parsing job result: type={type(job_result_post_processing).__name__}")
43
- job_response.job_result = parse(job_result_post_processing)
36
+ # RuntimePhase.POST_PROCESSING duoc dinh nghia nhung truoc day khong
37
+ # noi nao dung, nen code user chay trong post_processing_fn khong he
38
+ # co phase trong log lan trong ErrorReport.
39
+ with phase(RuntimePhase.POST_PROCESSING):
40
+ self.logger.info("Execute post_processing ...")
41
+ job_result_post_processing = self.post_processing_fn(
42
+ self.promise.job_result)
43
+
44
+ # print() cũ ghi thẳng ra stdout, không qua sink nên không được
45
+ # redact và không có job_id.
46
+ self.logger.debug(
47
+ f"Parsing job result: type={type(job_result_post_processing).__name__}")
48
+ job_response.job_result = parse(job_result_post_processing)
44
49
 
45
50
  update_job_metadata(job_response, self.promise.callback_url.on_done)
46
51
 
@@ -69,8 +69,16 @@ def bind_job(job_id: str = None, *, sdk=None, provider=None, device=None,
69
69
  @param trace_id: tự sinh nếu không truyền; dùng để join Function Log <-> Job Log
70
70
  @return: logger đã bind toàn bộ context
71
71
  """
72
+ # Một job bị bind hai lần (AsyncInvocationTask rồi Handler.__init__), trước
73
+ # đây mỗi lần sinh một trace_id mới nên cùng một job có hai traceId khác
74
+ # nhau và filter theo traceId mất các dòng ở cửa vào. Giữ lại trace đang có
75
+ # khi vẫn là job đó; job khác thì phải sinh trace mới, không được trộn.
76
+ current = _JOB_CONTEXT.get()
77
+ inherited = (current.get(TRACE_ID)
78
+ if current.get(JOB_ID) in (None, job_id) else None)
79
+
72
80
  context = {JOB_ID : job_id,
73
- TRACE_ID : trace_id or uuid.uuid4().hex[:16],
81
+ TRACE_ID : trace_id or inherited or uuid.uuid4().hex[:16],
74
82
  'sdk' : sdk,
75
83
  'provider' : provider,
76
84
  'device' : device,
@@ -8,6 +8,7 @@ from ...component.callback.update_job_metadata import update_job_metadata
8
8
  from ...data.callback.callback_url import CallbackUrl
9
9
  from ...data.response.job_response import JobResponse
10
10
  from ...enum.runtime_phase import RuntimePhase
11
+ from ...config.log_context import phase
11
12
  from ...model.device.device import Device
12
13
  from ...model.provider.provider import Provider
13
14
  from ...util.response_utils import build_error_job_response
@@ -34,21 +35,28 @@ class CustomDevice(Device, ABC):
34
35
  update_job_metadata(job_response, analysis_callback.on_start)
35
36
 
36
37
  try:
37
- self.logger.debug(
38
- "Producing histogram data from original job result")
39
-
40
- job_response.job_histogram = self._produce_histogram_data(
41
- original_job_result)
42
-
43
- self.logger.debug("Parsing original job result")
44
- original_job_result_dict = parse(original_job_result)
45
-
46
- self.logger.debug("Calculating execution time from parsed results")
47
- self._calculate_execution_time(original_job_result_dict)
48
- job_response.execution_time = self.execution_time
49
-
50
- self.logger.debug("Retrieving shot count from job result")
51
- job_response.shots = self._get_shots(original_job_result)
38
+ # Device._on_analysis co `with phase(...)`, ban override nay khong --
39
+ # va day moi la ban thuc su chay: QiskitDevice, OqcCloudDevice...
40
+ # deu ke thua CustomDevice. Thieu no thi log cua analysis khong co
41
+ # field `phase`, Grafana khong loc duoc va ErrorReport cua loi trong
42
+ # analysis ra phase=None.
43
+ with phase(RuntimePhase.ANALYSIS):
44
+ self.logger.debug(
45
+ "Producing histogram data from original job result")
46
+
47
+ job_response.job_histogram = self._produce_histogram_data(
48
+ original_job_result)
49
+
50
+ self.logger.debug("Parsing original job result")
51
+ original_job_result_dict = parse(original_job_result)
52
+
53
+ self.logger.debug(
54
+ "Calculating execution time from parsed results")
55
+ self._calculate_execution_time(original_job_result_dict)
56
+ job_response.execution_time = self.execution_time
57
+
58
+ self.logger.debug("Retrieving shot count from job result")
59
+ job_response.shots = self._get_shots(original_job_result)
52
60
 
53
61
  update_job_metadata(job_response, analysis_callback.on_done)
54
62
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.14.dev5
3
+ Version: 0.0.14.dev6
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -101,4 +101,5 @@ tests/test_language_dispatcher.py
101
101
  tests/test_log_secret_leak.py
102
102
  tests/test_logging_config.py
103
103
  tests/test_no_sdk_dependency.py
104
+ tests/test_phase_coverage.py
104
105
  tests/test_result_serializer.py
@@ -0,0 +1,196 @@
1
+ # Quapp Platform Project
2
+ # test_phase_coverage.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """Mọi giai đoạn của một job phải mang `phase` trong log và trong ErrorReport.
6
+
7
+ Ba bug bị khoá ở đây, cả ba đều phát hiện từ log thật của job
8
+ 4a5d2c56-f248-41e7-974f-a1bbea6db1c0 trên dev:
9
+
10
+ 1. `CustomDevice._on_analysis` không có `with phase(...)`. `Device._on_analysis`
11
+ thì có, nhưng bản CHẠY THẬT là bản override trong CustomDevice --
12
+ QiskitDevice, OqcCloudDevice... đều kế thừa nó. Log của analysis vì thế
13
+ không có field `phase`, và lỗi trong analysis ra `phase=None`.
14
+
15
+ 2. `RuntimePhase.POST_PROCESSING` được định nghĩa nhưng không nơi nào dùng, nên
16
+ code user chạy trong `post_processing_fn` không có phase.
17
+
18
+ 3. `bind_job()` sinh trace_id mới mỗi lần gọi. Một job bị bind hai lần
19
+ (AsyncInvocationTask rồi Handler.__init__) nên cùng một job có hai traceId,
20
+ filter theo traceId mất các dòng ở cửa vào.
21
+ """
22
+
23
+ import ast
24
+ import pathlib
25
+
26
+ import pytest
27
+ from loguru import logger
28
+ from quapp_common.config import log_context
29
+ from quapp_common.config.log_context import (
30
+ TRACE_ID, bind_job, effective_phase, trace_id,
31
+ )
32
+ from quapp_common.config.logging_config import _prepare_record, configure_logging
33
+ from quapp_common.enum.runtime_phase import RuntimePhase
34
+
35
+ PACKAGE_ROOT = pathlib.Path(__file__).resolve().parent.parent / 'quapp_common'
36
+
37
+
38
+ @pytest.fixture(autouse=True)
39
+ def clean_context():
40
+ log_context.reset()
41
+ yield
42
+ log_context.reset()
43
+ configure_logging(force=True)
44
+
45
+
46
+ @pytest.fixture
47
+ def captured():
48
+ records = []
49
+ logger.remove()
50
+ sink_id = logger.add(lambda message: records.append(message.record['extra']),
51
+ level='DEBUG', filter=_prepare_record)
52
+ yield records
53
+ logger.remove(sink_id)
54
+
55
+
56
+ def phases_wrapped_in(relative_path):
57
+ """Tập RuntimePhase được mở bằng `with phase(...)` trong một file."""
58
+ tree = ast.parse((PACKAGE_ROOT / relative_path).read_text(encoding='utf-8'))
59
+
60
+ found = set()
61
+ for node in ast.walk(tree):
62
+ if not isinstance(node, ast.With):
63
+ continue
64
+ for item in node.items:
65
+ call = item.context_expr
66
+ if not isinstance(call, ast.Call):
67
+ continue
68
+ name = getattr(call.func, 'id', None) or getattr(call.func, 'attr',
69
+ None)
70
+ if name != 'phase' or not call.args:
71
+ continue
72
+ argument = call.args[0]
73
+ if isinstance(argument, ast.Attribute):
74
+ found.add(argument.attr)
75
+ return found
76
+
77
+
78
+ # ------------------------------------------- (1) CustomDevice phai co ANALYSIS
79
+
80
+ def test_custom_device_analysis_is_wrapped():
81
+ """Ban override moi la ban chay that, no phai mo phase nhu ban goc."""
82
+ assert 'ANALYSIS' in phases_wrapped_in('model/device/custom_device.py'), (
83
+ 'CustomDevice._on_analysis khong co `with phase(RuntimePhase.ANALYSIS)`; '
84
+ 'moi device Qiskit dung ban nay nen log analysis se mat field phase')
85
+
86
+
87
+ def test_base_device_still_wraps_its_phases():
88
+ """Khong duoc lam hong ban goc khi sua ban override."""
89
+ wrapped = phases_wrapped_in('model/device/device.py')
90
+ assert {'COMPILATION', 'EXECUTION', 'ANALYSIS'} <= wrapped
91
+
92
+
93
+ # ------------------------------------ (2) post_processing phai co POST_PROCESSING
94
+
95
+ def test_post_processing_task_is_wrapped():
96
+ wrapped = phases_wrapped_in('async_tasks/post_processing_task.py')
97
+ assert 'POST_PROCESSING' in wrapped, (
98
+ 'post_processing_fn (code cua user) chay ngoai moi phase, nen loi trong '
99
+ 'do khong biet thuoc giai doan nao')
100
+
101
+
102
+ def test_every_declared_phase_is_actually_used():
103
+ """RuntimePhase khai bao ra thi phai co noi mo -- tru CALLBACK/POLLING.
104
+
105
+ CALLBACK va POLLING duoc dat qua `phase=` cua build_error_job_response chu
106
+ khong qua `with phase(...)`, nen khong tinh o day.
107
+ """
108
+ used = set()
109
+ for path in PACKAGE_ROOT.rglob('*.py'):
110
+ used |= phases_wrapped_in(path.relative_to(PACKAGE_ROOT))
111
+
112
+ declared = {member.name for member in RuntimePhase}
113
+ never_opened = declared - used - {'CALLBACK', 'POLLING'}
114
+ assert not never_opened, (
115
+ f'RuntimePhase khong bao gio duoc mo: {sorted(never_opened)}')
116
+
117
+
118
+ # --------------------------------------------------- hanh vi khi chay that
119
+
120
+ def test_analysis_phase_reaches_the_log_record(captured):
121
+ """Trong `with phase(ANALYSIS)`, dong log phai mang phase=ANALYSIS."""
122
+ from quapp_common.config.log_context import phase
123
+
124
+ bind_job('job_A', sdk='qiskit')
125
+ with phase(RuntimePhase.ANALYSIS):
126
+ logger.bind(**log_context.job_context()).debug('producing histogram')
127
+
128
+ record = next(r for r in captured if r.get('event') is None)
129
+ assert record.get('phase') == 'ANALYSIS'
130
+ assert record.get('job_id') == 'job_A'
131
+
132
+
133
+ def test_failure_inside_analysis_keeps_the_phase():
134
+ """`phase()` reset contextvar trong finally, nen ErrorReport phai doc duoc
135
+ phase da that bai qua _FAILED_PHASE."""
136
+ from quapp_common.config.log_context import phase
137
+
138
+ bind_job('job_B')
139
+ with pytest.raises(ValueError):
140
+ with phase(RuntimePhase.ANALYSIS):
141
+ raise ValueError('histogram vo')
142
+
143
+ assert effective_phase() is RuntimePhase.ANALYSIS
144
+
145
+
146
+ def test_failure_inside_post_processing_keeps_the_phase():
147
+ from quapp_common.config.log_context import phase
148
+
149
+ bind_job('job_C')
150
+ with pytest.raises(KeyError):
151
+ with phase(RuntimePhase.POST_PROCESSING):
152
+ raise KeyError('counts')
153
+
154
+ assert effective_phase() is RuntimePhase.POST_PROCESSING
155
+
156
+
157
+ # ------------------------------------------------- (3) trace_id khong bi tach
158
+
159
+ def test_rebinding_same_job_keeps_one_trace_id():
160
+ """AsyncInvocationTask bind roi Handler.__init__ bind lai -- van mot trace."""
161
+ bind_job('job_SAME')
162
+ first = trace_id()
163
+
164
+ bind_job('job_SAME', sdk='qiskit', provider='QUAO_QUANTUM_SIMULATOR')
165
+ assert trace_id() == first, 'cung mot job ma co hai traceId'
166
+
167
+
168
+ def test_binding_without_job_id_then_with_it_keeps_the_trace():
169
+ """Cua vao HTTP bind truoc khi biet job_id; trace phai duoc mang theo."""
170
+ bind_job(None)
171
+ first = trace_id()
172
+
173
+ bind_job('job_LATE')
174
+ assert trace_id() == first
175
+
176
+
177
+ def test_different_job_gets_a_fresh_trace_id():
178
+ """Khong duoc tron trace giua hai job khac nhau."""
179
+ bind_job('job_ONE')
180
+ first = trace_id()
181
+
182
+ bind_job('job_TWO')
183
+ assert trace_id() != first
184
+
185
+
186
+ def test_explicit_trace_id_always_wins():
187
+ bind_job('job_X')
188
+ bind_job('job_X', trace_id='deadbeefdeadbeef')
189
+ assert trace_id() == 'deadbeefdeadbeef'
190
+
191
+
192
+ def test_trace_id_is_present_on_every_record(captured):
193
+ bind_job('job_T')
194
+ logger.bind(**log_context.job_context()).info('mot dong bat ky')
195
+
196
+ assert captured[-1].get(TRACE_ID)