quantum-framework 0.18.0__py3-none-any.whl

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 (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,364 @@
1
+ """
2
+ Test Execution Service for Quantum Admin
3
+ Manages test execution using test_runner.py
4
+ """
5
+ import asyncio
6
+ import subprocess
7
+ import re
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional, Dict, AsyncGenerator
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TestExecutionService:
17
+ """Service for running Quantum tests and tracking results"""
18
+
19
+ # Store active test processes for cancellation
20
+ active_processes: Dict[int, asyncio.subprocess.Process] = {}
21
+
22
+ @staticmethod
23
+ async def run_tests(
24
+ db,
25
+ project_id: int,
26
+ suite_filter: Optional[str] = None,
27
+ verbose: bool = False,
28
+ stop_on_fail: bool = False,
29
+ triggered_by: str = "manual"
30
+ ) -> int:
31
+ """
32
+ Execute test_runner.py and track results
33
+
34
+ Args:
35
+ db: Database session
36
+ project_id: Project ID
37
+ suite_filter: Optional suite name to run (e.g., 'loops', 'query')
38
+ verbose: Enable verbose output
39
+ stop_on_fail: Stop on first failure
40
+ triggered_by: Who triggered the test
41
+
42
+ Returns:
43
+ int: Test run ID
44
+
45
+ Raises:
46
+ Exception: If test execution fails
47
+ """
48
+ # Import models here to avoid circular imports
49
+ try:
50
+ from . import models
51
+ except ImportError:
52
+ import models
53
+
54
+ # Create test run record
55
+ test_run = models.TestRun(
56
+ project_id=project_id,
57
+ status='running',
58
+ started_at=datetime.utcnow(),
59
+ total_tests=0,
60
+ passed_tests=0,
61
+ failed_tests=0,
62
+ triggered_by=triggered_by,
63
+ suite_filter=suite_filter
64
+ )
65
+ db.add(test_run)
66
+ db.commit()
67
+ db.refresh(test_run)
68
+
69
+ logger.info(f"🧪 Starting test run {test_run.id} for project {project_id}")
70
+
71
+ # Build command
72
+ project_root = Path(__file__).parent.parent.parent # Go up to quantum/ directory
73
+ test_runner_path = project_root / "test_runner.py"
74
+
75
+ cmd = ['python', str(test_runner_path)]
76
+ if suite_filter:
77
+ cmd.extend(['--feature', suite_filter])
78
+ if verbose:
79
+ cmd.append('--verbose')
80
+
81
+ logger.info(f"📋 Command: {' '.join(cmd)}")
82
+
83
+ try:
84
+ # Execute tests asynchronously
85
+ process = await asyncio.create_subprocess_exec(
86
+ *cmd,
87
+ stdout=asyncio.subprocess.PIPE,
88
+ stderr=asyncio.subprocess.PIPE,
89
+ cwd=str(project_root)
90
+ )
91
+
92
+ # Store process for potential cancellation
93
+ TestExecutionService.active_processes[test_run.id] = process
94
+
95
+ # Wait for completion
96
+ stdout, stderr = await process.communicate()
97
+
98
+ # Remove from active processes
99
+ TestExecutionService.active_processes.pop(test_run.id, None)
100
+
101
+ # Parse test results
102
+ output = stdout.decode('utf-8')
103
+ results = TestExecutionService._parse_test_output(output)
104
+
105
+ # Update test run
106
+ test_run.status = 'completed' if process.returncode == 0 else 'failed'
107
+ test_run.completed_at = datetime.utcnow()
108
+ test_run.total_tests = results['total']
109
+ test_run.passed_tests = results['passed']
110
+ test_run.failed_tests = results['failed']
111
+ test_run.duration_seconds = int((test_run.completed_at - test_run.started_at).total_seconds())
112
+
113
+ # Save individual test results
114
+ for test in results['tests']:
115
+ test_result = models.TestResult(
116
+ test_run_id=test_run.id,
117
+ suite_name=test['suite'],
118
+ test_file=test['file'],
119
+ status=test['status'],
120
+ duration_seconds=test.get('duration', 0),
121
+ error_message=test.get('error'),
122
+ output=test.get('output')
123
+ )
124
+ db.add(test_result)
125
+
126
+ db.commit()
127
+
128
+ logger.info(f"✅ Test run {test_run.id} completed: {test_run.passed_tests}/{test_run.total_tests} passed")
129
+ return test_run.id
130
+
131
+ except asyncio.CancelledError:
132
+ # Handle cancellation
133
+ test_run.status = 'cancelled'
134
+ test_run.completed_at = datetime.utcnow()
135
+ test_run.duration_seconds = int((test_run.completed_at - test_run.started_at).total_seconds())
136
+ db.commit()
137
+
138
+ logger.info(f"⚠️ Test run {test_run.id} cancelled")
139
+ raise
140
+
141
+ except Exception as e:
142
+ test_run.status = 'error'
143
+ test_run.error_message = str(e)
144
+ test_run.completed_at = datetime.utcnow()
145
+ test_run.duration_seconds = int((test_run.completed_at - test_run.started_at).total_seconds())
146
+ db.commit()
147
+
148
+ logger.error(f"❌ Test run {test_run.id} failed: {e}")
149
+ raise
150
+
151
+ @staticmethod
152
+ def _parse_test_output(output: str) -> Dict:
153
+ """
154
+ Parse test_runner.py output and extract results
155
+
156
+ Args:
157
+ output: Raw stdout from test_runner.py
158
+
159
+ Returns:
160
+ dict: Parsed results with structure:
161
+ {
162
+ 'total': int,
163
+ 'passed': int,
164
+ 'failed': int,
165
+ 'tests': [
166
+ {
167
+ 'suite': str,
168
+ 'file': str,
169
+ 'status': 'passed'|'failed',
170
+ 'error': str (optional),
171
+ 'output': str (optional)
172
+ }
173
+ ]
174
+ }
175
+ """
176
+ results = {
177
+ 'total': 0,
178
+ 'passed': 0,
179
+ 'failed': 0,
180
+ 'tests': []
181
+ }
182
+
183
+ # Parse summary line: "TOTAL: 97/97 tests passed (100.0%)"
184
+ summary_match = re.search(r'TOTAL: (\d+)/(\d+) tests passed', output)
185
+ if summary_match:
186
+ results['passed'] = int(summary_match.group(1))
187
+ results['total'] = int(summary_match.group(2))
188
+ results['failed'] = results['total'] - results['passed']
189
+
190
+ # Parse individual test results
191
+ current_suite = None
192
+
193
+ # Look for feature headers: "Feature: Loops"
194
+ for line in output.split('\n'):
195
+ feature_match = re.match(r'Feature: (.+)', line)
196
+ if feature_match:
197
+ current_suite = feature_match.group(1).strip()
198
+ continue
199
+
200
+ # Parse test results: " PASS test-loop-basic.q" or " FAIL test-loop-error.q"
201
+ test_match = re.match(r'\s+(PASS|FAIL)\s+(test-.+\.q)(?:\s+\((.+)\))?', line)
202
+ if test_match and current_suite:
203
+ status = test_match.group(1).lower()
204
+ filename = test_match.group(2)
205
+ note = test_match.group(3) # e.g., "expected failure"
206
+
207
+ test_info = {
208
+ 'suite': current_suite,
209
+ 'file': filename,
210
+ 'status': status,
211
+ 'error': None,
212
+ 'output': None
213
+ }
214
+
215
+ # Handle expected failures
216
+ if note and 'expected failure' in note:
217
+ # Expected failure that failed = PASS (test is correct)
218
+ if status == 'fail':
219
+ test_info['status'] = 'passed'
220
+ # Expected failure that passed = FAIL (test is wrong)
221
+ elif status == 'pass':
222
+ test_info['status'] = 'failed'
223
+ test_info['error'] = 'Should have failed but passed'
224
+
225
+ results['tests'].append(test_info)
226
+
227
+ # Parse error messages (lines starting with " Error:")
228
+ for i, line in enumerate(output.split('\n')):
229
+ if line.strip().startswith('Error:'):
230
+ error_msg = line.strip()[7:] # Remove "Error: " prefix
231
+ # Find the previous test result and add error
232
+ if results['tests']:
233
+ results['tests'][-1]['error'] = error_msg
234
+
235
+ return results
236
+
237
+ @staticmethod
238
+ async def cancel_test_run(db, test_run_id: int) -> bool:
239
+ """
240
+ Cancel a running test execution
241
+
242
+ Args:
243
+ db: Database session
244
+ test_run_id: Test run ID to cancel
245
+
246
+ Returns:
247
+ bool: True if cancelled successfully, False otherwise
248
+ """
249
+ try:
250
+ from . import models
251
+ except ImportError:
252
+ import models
253
+
254
+ # Check if test run exists and is running
255
+ test_run = db.query(models.TestRun).filter(
256
+ models.TestRun.id == test_run_id
257
+ ).first()
258
+
259
+ if not test_run:
260
+ logger.warning(f"Test run {test_run_id} not found")
261
+ return False
262
+
263
+ if test_run.status != 'running':
264
+ logger.warning(f"Test run {test_run_id} is not running (status: {test_run.status})")
265
+ return False
266
+
267
+ # Get process if active
268
+ process = TestExecutionService.active_processes.get(test_run_id)
269
+ if process:
270
+ try:
271
+ process.terminate()
272
+ await asyncio.wait_for(process.wait(), timeout=5.0)
273
+ except asyncio.TimeoutError:
274
+ process.kill()
275
+
276
+ TestExecutionService.active_processes.pop(test_run_id, None)
277
+
278
+ # Update database
279
+ test_run.status = 'cancelled'
280
+ test_run.completed_at = datetime.utcnow()
281
+ test_run.duration_seconds = int((test_run.completed_at - test_run.started_at).total_seconds())
282
+ db.commit()
283
+
284
+ logger.info(f"✅ Test run {test_run_id} cancelled")
285
+ return True
286
+
287
+ @staticmethod
288
+ def get_test_run_status(db, test_run_id: int) -> Optional[Dict]:
289
+ """
290
+ Get current status of a test run
291
+
292
+ Args:
293
+ db: Database session
294
+ test_run_id: Test run ID
295
+
296
+ Returns:
297
+ dict: Status information or None if not found
298
+ """
299
+ try:
300
+ from . import models
301
+ except ImportError:
302
+ import models
303
+
304
+ test_run = db.query(models.TestRun).filter(
305
+ models.TestRun.id == test_run_id
306
+ ).first()
307
+
308
+ if not test_run:
309
+ return None
310
+
311
+ # Calculate progress
312
+ progress_percentage = 0.0
313
+ if test_run.total_tests > 0:
314
+ completed = test_run.passed_tests + test_run.failed_tests
315
+ progress_percentage = (completed / test_run.total_tests) * 100
316
+
317
+ # Get current suite (from latest test result)
318
+ current_suite = None
319
+ if test_run.test_results:
320
+ latest_result = sorted(test_run.test_results, key=lambda r: r.id)[-1]
321
+ current_suite = latest_result.suite_name
322
+
323
+ # Estimate time remaining (rough estimate)
324
+ estimated_time_remaining = None
325
+ if test_run.status == 'running' and progress_percentage > 0:
326
+ elapsed = (datetime.utcnow() - test_run.started_at).total_seconds()
327
+ total_estimated = elapsed / (progress_percentage / 100)
328
+ estimated_time_remaining = int(total_estimated - elapsed)
329
+
330
+ return {
331
+ 'id': test_run.id,
332
+ 'status': test_run.status,
333
+ 'total_tests': test_run.total_tests,
334
+ 'passed_tests': test_run.passed_tests,
335
+ 'failed_tests': test_run.failed_tests,
336
+ 'current_suite': current_suite,
337
+ 'progress_percentage': progress_percentage,
338
+ 'estimated_time_remaining': estimated_time_remaining
339
+ }
340
+
341
+ @staticmethod
342
+ async def stream_test_output(test_run_id: int) -> AsyncGenerator[str, None]:
343
+ """
344
+ Stream test output in real-time (for future SSE implementation)
345
+
346
+ Args:
347
+ test_run_id: Test run ID
348
+
349
+ Yields:
350
+ str: Log lines as they are generated
351
+ """
352
+ # Get process if active
353
+ process = TestExecutionService.active_processes.get(test_run_id)
354
+ if not process or not process.stdout:
355
+ return
356
+
357
+ try:
358
+ while True:
359
+ line = await process.stdout.readline()
360
+ if not line:
361
+ break
362
+ yield line.decode('utf-8')
363
+ except Exception as e:
364
+ logger.error(f"Error streaming output for test run {test_run_id}: {e}")