From bf263a667d48343396f9dc2e84e8e5feb236c266 Mon Sep 17 00:00:00 2001 From: longpeng Date: Fri, 27 Jun 2025 18:07:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=85=B3=E9=97=AD=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E4=BC=9A=E8=AF=9D=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=B5=8F=E8=A7=88=E5=99=A8=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=EF=BC=8C=E4=BC=98=E5=8C=96UI=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proto/common_test.proto | 7 +++ workers/python/activities.py | 10 +++- workers/python/main.py | 2 +- workers/python/ui_tests.py | 103 ++++++++++++++++++++++------------ workflows/dynamic_workflow.go | 31 ++++++++-- 5 files changed, 111 insertions(+), 42 deletions(-) diff --git a/proto/common_test.proto b/proto/common_test.proto index bdb9346..030e900 100644 --- a/proto/common_test.proto +++ b/proto/common_test.proto @@ -33,6 +33,12 @@ message UiTestRequest { string browser_type = 3; // "chromium", "firefox", "webkit" bool headless = 4; // 是否无头模式 map user_data = 5; // 用户名、密码等敏感信息,不建议直接传输,建议 Worker 端获取 + optional string browser_session_id = 6; // 可选,用于复用现有浏览器会话 +} + +// 请求关闭浏览器会话 +message CloseBrowserRequest { + string browser_session_id = 1; // 要关闭的浏览器会话ID } // -------- 结果消息 -------- @@ -59,6 +65,7 @@ message UiTestResult { BaseTestResult base_result = 1; string screenshot_url = 2; // 截图存储URL (worker上传后返回) string html_report_url = 3; // HTML报告URL + string browser_session_id = 4; // 返回创建或复用的浏览器会话ID } // 整个测试任务的输出结果 diff --git a/workers/python/activities.py b/workers/python/activities.py index 4a2fd82..ecdf948 100644 --- a/workers/python/activities.py +++ b/workers/python/activities.py @@ -11,7 +11,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gen' # 导入protobuf生成的模块和其他依赖 from pb import common_test_pb2 as pb from api_tests import execute_api_test_case -from ui_tests import execute_ui_test_case +from ui_tests import execute_ui_test_case, close_browser_session from utils import upload_file_to_s3, scalar_map_to_dict @@ -178,3 +178,11 @@ class TestActivities: # 计算并记录测试执行时长 result.base_result.duration_seconds = time.time() - start_time return result + + @activity.defn(name="CloseBrowser") + async def close_browser(self, req: pb.CloseBrowserRequest) -> dict: + """ + 关闭指定的浏览器会话 + """ + success, msg = await close_browser_session(req.browser_session_id) + return {"success": success, "message": msg} diff --git a/workers/python/main.py b/workers/python/main.py index bffd6fa..3427069 100644 --- a/workers/python/main.py +++ b/workers/python/main.py @@ -20,7 +20,7 @@ async def main(): worker = Worker( client, task_queue="python-task-queue", # 保持与 Go Client 一致 - activities=[activities.run_api_test,activities.run_ui_test] + activities=[activities.run_api_test,activities.run_ui_test,activities.close_browser] ) print("Starting Python Temporal Worker...") await worker.run() diff --git a/workers/python/ui_tests.py b/workers/python/ui_tests.py index 27ccda5..a7f53f6 100644 --- a/workers/python/ui_tests.py +++ b/workers/python/ui_tests.py @@ -2,12 +2,19 @@ from playwright.async_api import async_playwright, expect import os import datetime +import uuid +from typing import Optional -async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: str, headless: bool, user_data: dict): +# 全局浏览器会话管理 +_browser_sessions = {} +_playwright_instance = None + +async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: str, headless: bool, user_data: dict, browser_session_id: Optional[str] = None): """ 实际执行UI测试的函数。 - 这里使用 Playwright,你也可以替换成 Selenium。 + 支持浏览器会话复用。 """ + global _browser_sessions, _playwright_instance base_url = "https://playwright.dev" # 假设 UI 测试的基地址 full_url = f"{base_url}{url_path}" @@ -15,51 +22,61 @@ async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: s success = False screenshot_path = None html_report_path = None - - browser = None page = None + created_new_session = False + session_id = browser_session_id log_output.append(f"Executing UI test: {test_case_id} - {full_url} with {browser_type}") try: - async with async_playwright() as p: + if _playwright_instance is None: + _playwright_instance = await async_playwright().start() + + # 浏览器复用逻辑 + if session_id and session_id in _browser_sessions: + browser = _browser_sessions[session_id] + log_output.append(f"Reusing browser session: {session_id}") + else: + # 新建浏览器 if browser_type == "chromium": - browser = await p.chromium.launch(headless=headless) + browser = await _playwright_instance.chromium.launch(headless=headless) elif browser_type == "firefox": - browser = await p.firefox.launch(headless=headless) + browser = await _playwright_instance.firefox.launch(headless=headless) elif browser_type == "webkit": - browser = await p.webkit.launch(headless=headless) + browser = await _playwright_instance.webkit.launch(headless=headless) else: raise ValueError(f"Unsupported browser type: {browser_type}") + # 生成新的 session_id + session_id = str(uuid.uuid4()) + _browser_sessions[session_id] = browser + created_new_session = True + log_output.append(f"Created new browser session: {session_id}") - page = await browser.new_page() + page = await browser.new_page() - # 模拟登录(如果需要) - if user_data: - log_output.append(f"Attempting to log in with user: {user_data.get('user')}") - # 假设有一个登录页面 - await page.goto(f"{base_url}/login") - await page.fill('input[name="username"]', user_data.get('user', '')) - await page.fill('input[name="password"]', user_data.get('pass', '')) - await page.click('button[type="submit"]') - await page.wait_for_url(full_url) # 等待跳转到目标页面 + # 模拟登录(如果需要) + if user_data: + log_output.append(f"Attempting to log in with user: {user_data.get('user')}") + await page.goto(f"{base_url}/login") + await page.fill('input[name="username"]', user_data.get('user', '')) + await page.fill('input[name="password"]', user_data.get('pass', '')) + await page.click('button[type="submit"]') + await page.wait_for_url(full_url) - await page.goto(full_url) - log_output.append(f"Navigated to: {full_url}") + await page.goto(full_url) + log_output.append(f"Navigated to: {full_url}") - # 示例UI操作和断言 - # 查找一个元素并验证其文本 - element = page.locator("text=Playwright enables reliable end-to-end testing for modern web apps.") - await expect(element).to_be_visible() - log_output.append("Found expected text on page.") + # 示例UI操作和断言 + element = page.locator("text=Playwright enables reliable end-to-end testing for modern web apps.") + await expect(element).to_be_visible() + log_output.append("Found expected text on page.") - # 点击一个链接 - await page.click("text=Docs") - await page.wait_for_url("**/docs/intro") - log_output.append("Clicked 'Docs' link and navigated.") + await page.click("text=Docs") + await page.wait_for_url("**/docs/intro") + log_output.append("Clicked 'Docs' link and navigated.") - success = True - log_output.append("UI Test PASSED.") + success = True + log_output.append("UI Test PASSED.") except Exception as e: log_output.append(f"UI Test FAILED (Exception): {e}") @@ -74,11 +91,27 @@ async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: s log_output.append(f"Screenshot saved to: {screenshot_path}") except Exception as e: log_output.append(f"Failed to take screenshot: {e}") - - if browser: + # 注意:只有不是复用时才自动关闭浏览器 + if created_new_session and session_id in _browser_sessions: try: - await browser.close() + await _browser_sessions[session_id].close() + del _browser_sessions[session_id] + log_output.append(f"Closed browser session: {session_id}") except Exception as e: log_output.append(f"Failed to close browser: {e}") - return success, "\n".join(log_output), screenshot_path, html_report_path \ No newline at end of file + return success, "\n".join(log_output), screenshot_path, html_report_path, session_id + +async def close_browser_session(browser_session_id: str): + """ + 显式关闭指定的浏览器会话。 + """ + global _browser_sessions + if browser_session_id in _browser_sessions: + try: + await _browser_sessions[browser_session_id].close() + del _browser_sessions[browser_session_id] + return True, f"Closed browser session: {browser_session_id}" + except Exception as e: + return False, f"Failed to close browser: {e}" + return False, f"No such browser session: {browser_session_id}" \ No newline at end of file diff --git a/workflows/dynamic_workflow.go b/workflows/dynamic_workflow.go index 9a3c285..2f2dacb 100644 --- a/workflows/dynamic_workflow.go +++ b/workflows/dynamic_workflow.go @@ -72,11 +72,12 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu // 步骤3: 初始化执行状态和结果收集器 // ======================================================================================== var ( - overallSuccess = true // 整体执行成功标志,任何一个步骤失败都会置为 false - apiResults []*pb.ApiTestResult // 收集所有 API 测试结果 - uiResults []*pb.UiTestResult // 收集所有 UI 测试结果 - stepResults = make(map[string]bool) // 存储每个步骤的成功/失败状态,用于条件跳转判断 - currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转 + overallSuccess = true // 整体执行成功标志,任何一个步骤失败都会置为 false + apiResults []*pb.ApiTestResult // 收集所有 API 测试结果 + uiResults []*pb.UiTestResult // 收集所有 UI 测试结果 + stepResults = make(map[string]bool) // 存储每个步骤的成功/失败状态,用于条件跳转判断 + currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转 + lastBrowserSessionID string // 用于UI测试的浏览器会话ID ) // 初始化全局变量,包含输入参数中的全局参数 @@ -161,6 +162,10 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu overallSuccess = false break // 参数解析失败,跳出当前步骤 } + // 注入 browser_session_id + if lastBrowserSessionID != "" { + uiReq.BrowserSessionId = &lastBrowserSessionID + } activityInput = uiReq activityResult = &pb.UiTestResult{} // 预创建结果容器 @@ -258,6 +263,10 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu Data: res, } parameterProcessor.AddActivityResult(step.StepId, activityResult) + // 记录 browser_session_id 以便下一个 UI 步骤复用 + if res.BrowserSessionId != "" { + lastBrowserSessionID = res.BrowserSessionId + } // 可以在这里添加更多结果类型的处理 } logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed) @@ -300,6 +309,18 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu "apiResultsCount", len(apiResults), "uiResultsCount", len(uiResults)) + // 工作流结束时关闭浏览器会话 + if lastBrowserSessionID != "" { + closeReq := &pb.CloseBrowserRequest{BrowserSessionId: lastBrowserSessionID} + closeCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + TaskQueue: "python-task-queue", + StartToCloseTimeout: 2 * time.Minute, + }) + var closeResp interface{} + _ = workflow.ExecuteActivity(closeCtx, "CloseBrowser", closeReq).Get(closeCtx, &closeResp) + logger.Info("Closed browser session at workflow end", "browserSessionId", lastBrowserSessionID) + } + // 返回包含所有测试结果和执行状态的输出结构 return &pb.TestRunOutput{ RunId: input.RunId, // 运行标识符