在 AWS Lambda 上使用 Playwright 运行端到端测试
我以前一直在使用Selenium来满足我的一些端到端测试需求,但是对于最新的项目,我想看看是否有任何替代方案更适合我的需求。 Selenium 长期以来一直是浏览器自动化的首选工具。它非常强大,但也需要花费大量时间来掌握和配置。经过一番谷歌搜索,我确定了三个最有可能的选择:
-
傀儡师
-
剧作家
-
赛普拉斯
Puppeteer(由 Google 提供)使用 DevTools 协议和 Chromium(也为 Firefox 提供实验性支持)使其不太有趣,而 Playwright(由 Microsoft)支持 Chromium、Firefox 和 WebKit。 Playwright 背后的核心团队实际上与最初在 Puppeteer 上工作的团队相同。虽然前两个更多的是通用的浏览器自动化框架,但赛普拉斯的开发重点是端到端测试。 Cypress 提供了一个开源测试运行程序,支持 Chromium 和 Firefox,以及一个用于监控测试运行的闭源商业仪表板。
在设置了一些测试项目并浏览了文档后,我得出的结论是 Playwright 将提供最好的浏览器支持和最大的灵活性。所以我继续做一些更多的测试。不幸的是,您仍然需要一些基础设施来托管您的 Playwright 设置。你可以很容易地将它集成到你的 CI 过程中,但也许这不是你想要的。您的用例可能是其他东西,例如监控网站或测试没有 CI 管道的产品(也许我会在另一篇博文中详细介绍我的用例)。
作为一个无服务器爱好者,我想知道 Playwright 是否可以在 AWS Lambda 上运行。在这篇博文中,我将逐步介绍如何做到这一点。
容器镜像作为 AWS Lambda 函数
尽管可以将额外的依赖项(,例如 Chromium 二进制文件)作为层添加到 AWS Lambda,但维护起来有些困难。幸运的是,亚马逊刚刚在 12 月(2020 年)推出了一项新功能,该功能允许您将 Lambda 函数打包并部署为容器镜像](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/)— 在 AWS Lambda 上运行 Playwright 的游戏规则改变者!
因此,我开始考虑构建一个包含所有 Playwright 依赖项的容器映像,并将其部署为 Lambda 函数。
代码
这篇博文中提供的代码的最终版本可以从我的 GitHub 存储库https://github.com/lari/playwright-aws-lambda-example (v0.0.1)中找到。
项目目录如下所示:
playwright-aws-lambda-example/
- function/
| - index.js
| - package.json
- Dockerfile
- entrypoint.sh
要求
-
码头工人
-
AWS 命令行界面安装和配置(如果部署到 AWS)
函数
我创建了一个简单的 Node.js 函数,它使用 Playwright 打开 Google 搜索首页并将页面标题打印到控制台。您可以将浏览器(webkit、chromium 或 firefox)定义为名为browser的事件参数。
const { webkit, chromium, firefox } = require('playwright');
const browserTypes = {
'webkit': webkit,
'chromium': chromium,
'firefox': firefox,
};
exports.handler = async (event, context) => {
let browserName = event.browser || 'chromium';
let browser;
try {
browser = await browserTypes[browserName].launch({});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('http://google.com/');
console.log(`Page title: ${await page.title()}`);
} catch (error) {
console.log(`Error ${error}`);
throw error;
} finally {
if (browser) {
await browser.close();
}
}
}
记得创建package.json并添加playwright作为依赖项。
Dockerfile
我开始构建基于这个帖子在 AWS 博客上的 docker 镜像。
让我们使用mcr.microsoft.com/playwright:focal作为基础镜像并安装 AWS Lambda Runtime API 的构建依赖项。
FROM mcr.microsoft.com/playwright:focal as build-image
# Install aws-lambda-ric build dependencies
RUN apt-get update && apt-get install -y \
g++ \
make \
cmake \
unzip \
libcurl4-openssl-dev \
autoconf \
libtool
接下来,我们将函数目录(实际的 lambda 函数代码)复制到映像并安装 Node.js 包。我们还需要安装Node.js 运行时接口客户端 (RIC)。
# Define custom function directory
ARG FUNCTION_DIR="/function"
# Create function dir and install node packages
RUN mkdir -p ${FUNCTION_DIR}
COPY function/ ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}
RUN npm install
RUN npm install aws-lambda-ric
最后,我们将添加Lambda Runtime Interface Emulator (RIE)来帮助我们进行本地测试。 RIE 基本上允许您通过 HTTP 请求在本地运行的容器上调用您的函数。
# Add Lambda Runtime Interface Emulator and use a script in the ENTRYPOINT for simpler local runs
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin/aws-lambda-rie
RUN chmod 755 /usr/local/bin/aws-lambda-rie
COPY entrypoint.sh /
RUN chmod 755 /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
# Set function handler
CMD ["index.handler"]
入口点
entrypoint.sh脚本检查AWS_LAMBDA_RUNTIME_API环境变量是否存在。如果找到(即在真实的 AWS Lambda 环境中运行),则脚本正常启动 RIC,否则通过模拟器启动 RIC。 CMD 指令(函数处理程序)将作为参数传递给 ENTRYPOINT(运行时接口客户端)。
#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
exec /usr/local/bin/aws-lambda-rie /usr/bin/npx aws-lambda-ric $1
else
exec /usr/bin/npx aws-lambda-ric $1
fi
运行你的代码
本地使用 Docker
构建 docker 容器镜像:
docker build -t playwright-aws-lambda-example:latest .
运行容器镜像并将端口 9000 映射到容器的端口 8080:
docker run -p 9000:8080 playwright-aws-lambda-example:latest
使用 HTTP 请求调用函数。这是可能的,因为我们添加了运行时接口模拟器:
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
部署到 AWS Lambda
在以下示例中,将YOUR_AWS_ACCOUNT_ID替换为您的 AWS 账户 ID。
在 AWS ECR(弹性容器注册表)上创建存储库:
aws ecr create-repository --repository-name playwright-aws-lambda-example --image-scanning-configuration scanOnPush=true
标记 ECR 存储库的 docker 映像:
docker tag playwright-aws-lambda-example:latest YOUR_AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/playwright-aws-lambda-example:latest
登录:
aws ecr get-login-password | docker login --username AWS --password-stdin YOUR_AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com
将图像推送到 ECR:
docker push YOUR_AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/playwright-aws-lambda-example
最后,您需要创建一个 AWS Lambda 函数并使用您刚刚上传的容器映像部署它。这可以在 AWS Web 控制台中完成。
好得令人难以置信?让有问题...
一切似乎在您的本地 docker 容器上运行良好,但在将您的辛勤工作部署到 AWS Lambda 之后,您会注意到事情从未如此简单。接下来,我将介绍我在 AWS Lambda 环境中遇到的问题,并解释它们是如何解决的。
浏览器可执行路径
该函数将在实际 AWS Lambda 环境中失败并显示错误消息:
Failed to launch webkit because executable doesn't exist at
/home/sbx_user1051/.cache/ms-playwright/webkit-1402/pw_run.sh
基本映像 (mcr.microsoft.com/playwright:focal) 将浏览器可执行文件安装在名为pwuser的用户的主目录下,并为其他用户创建符号链接。但是,AWS Lambda 使用自己的名为sbx_userXXXX(sbx 可能代表沙盒)的用户池,这些用户池在构建映像时并不存在。因此,用户不会拥有指向他们自己的主目录下的浏览器可执行文件的符号链接。
幸运的是,Playwright 在browserType.launch() 方法中为浏览器可执行文件提供了自定义路径选项。所以我创建了一个辅助函数,它采用预期的浏览器可执行路径并返回/home/pwuser/目录下的路径:
getCustomExecutablePath = (expectedPath) => {
const suffix = expectedPath.split('/.cache/ms-playwright/')[1];
return `/home/pwuser/.cache/ms-playwright/${suffix}`;
}
然后,您可以将其用于executablePath启动选项:
browser = await browserTypes[browserName].launch({
executablePath: getCustomExecutablePath(browserTypes[browserName].executablePath()),
});
WebKit 可以正常工作
提供正确的可执行路径后,WebKit 浏览器似乎开箱即用。完全没有问题! (到目前为止。一些更特殊的用例可能存在问题)。
不支持多处理(?)
使用 Chromium 运行该函数时,它在名为“start_thread”的进程中失败。我不是这方面的专家,但我知道当 Chromium 试图为某些东西(可能与 GPU 相关)生成新线程/进程时,该函数会失败。
这是堆栈跟踪的一部分:
[err] #0 0x5588292260b3 content::internal::ChildProcessLauncherHelper::PostLaunchOnLauncherThread()
[err] #1 0x558829225b4c content::internal::ChildProcessLauncherHelper::StartLaunchOnClientThread()
[err] #2 0x55882976e952 content::VizProcessTransportFactory::ConnectHostFrameSinkManager()
[err] #3 0x55882b45541b mojo::SimpleWatcher::Context::Notify()
[err] #4 0x55882976e952 content::VizProcessTransportFactory::ConnectHostFrameSinkManager()
[err] Task trace buffer limit hit, update PendingTask::kTaskBacktraceLength to increase.
[err]
[err] Received signal 6
[err] #0 0x55882ad34919 base::debug::CollectStackTrace()
[err] #1 0x55882aca56d3 base::debug::StackTrace::StackTrace()
[err] #2 0x55882ad344fb base::debug::(anonymous namespace)::StackDumpSignalHandler()
[err] #3 0x7f5bba2093c0 (/usr/lib/x86_64-linux-gnu/libpthread-2.31.so+0x153bf)
[err] #4 0x7f5bb8a6e18b gsignal
[err] #5 0x7f5bb8a4d859 abort
[err] #6 0x55882ad33495 base::debug::BreakDebugger()
[err] #7 0x55882acb6d6d logging::LogMessage::~LogMessage()
[err] #8 0x5588293372f7 content::(anonymous namespace)::IntentionallyCrashBrowserForUnusableGpuProcess()
[err] #9 0x55882933504e content::GpuDataManagerImplPrivate::FallBackToNextGpuMode()
[err] #10 0x5588293339e3 content::GpuDataManagerImpl::FallBackToNextGpuMode()
[err] #11 0x55882933e11f content::GpuProcessHost::RecordProcessCrash()
[err] #12 0x5588291c46c3 content::BrowserChildProcessHostImpl::OnProcessLaunchFailed()
[err] #13 0x558829226250 content::internal::ChildProcessLauncherHelper::PostLaunchOnClientThread()
[err] #14 0x558829226495 base::internal::Invoker<>::RunOnce()
[err] #15 0x55882acf57a6 base::TaskAnnotator::RunTask()
[err] #16 0x55882ad06e13 base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl()
[err] #17 0x55882ad06aea base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork()
[err] #18 0x55882ad5af47 base::MessagePumpLibevent::Run()
[err] #19 0x55882ad0769b base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run()
[err] #20 0x55882acdd9cd base::RunLoop::Run()
[err] #21 0x5588291e1ec8 content::BrowserProcessSubThread::IOThreadRun()
[err] #22 0x55882ad1f014 base::Thread::ThreadMain()
[err] #23 0x55882ad450bf base::(anonymous namespace)::ThreadFunc()
[err] #24 0x7f5bba1fd609 start_thread
[err] #25 0x7f5bb8b4a293 clone
[err] r8: 0000000000000000 r9: 00007f5bb440ed30 r10: 0000000000000008 r11: 0000000000000246
[err] r12: 00007f5bb4410058 r13: 00007f5bb4410050 r14: 00007f5bb4410040 r15: 00007f5bb440f800
[err] di: 0000000000000002 si: 00007f5bb440ed30 bp: 00007f5bb440ef80 bx: 00007f5bb4411700
[err] dx: 0000000000000000 ax: 0000000000000000 cx: 00007f5bb8a6e18b sp: 00007f5bb440ed30
[err] ip: 00007f5bb8a6e18b efl: 0000000000000246 cgf: 002b000000000033 erf: 0000000000000000
[err] trp: 0000000000000000 msk: 0000000000000000 cr2: 0000000000000000
[err] [end of stack trace]
[err] Calling _exit(1). Core file will not be generated.
AWS Lambda 在有几个限制的环境中运行。其中之一似乎是缺乏多处理支持。现代浏览器使用几种不同的策略生成多个进程。例如。渲染通常使用自己的进程完成,以便在页面渲染缓慢或卡顿时不会阻塞其他事情。一些浏览器还为每个选项卡设置了单独的进程。您可以在此处阅读有关Chromium 进程模型的更多信息。
幸运的是,使用 Chromium,您可以使用名为--single-process的启动标志来禁用多个进程的使用。 “在这个模型中,浏览器和渲染引擎都在一个操作系统进程中运行。”同样,browserType.launch() 方法有一个名为args的选项:“传递给浏览器实例的附加参数”。
值得注意的是文档对这个--single-process标志的说法:“它不是一个安全或健壮的架构,因为任何渲染器崩溃都会导致整个浏览器进程的丢失。它是为测试和开发目的而设计的,它可能包含错误其他架构中不存在的。”
让我们为每种浏览器类型添加一个带有启动参数的常量:
const browserLaunchArgs = {
'webkit': [],
'chromium': [
'--single-process',
],
'firefox': [],
}
然后使用args选项启动浏览器:
browser = await browserTypes[browserName].launch({
executablePath: getCustomExecutablePath(browserTypes[browserName].executablePath()),
args: browserLaunchArgs[browserName],
});
火狐...还没解决
使用 Firefox,您会得到另一个神秘的堆栈跟踪,如下所示:
cloned child 31
[err] ExceptionHandler::SendContinueSignalToChild sent continue signal to child
[err] ExceptionHandler::WaitForContinueSignal waiting for continue signal...
[err] Unable to init server: Could not connect: Connection refused
<process did exit: exitCode=null, signal=SIGSEGV>
=========================== logs ===========================
<launching> /home/pwuser/.cache/ms-playwright/firefox-1221/firefox/firefox -no-remote -headless -profile /tmp/playwright_firefoxdev_profile-xmDdON -juggler-pipe -silent
<launched> pid=20
[err] *** You are running in headless mode.
[err] ExceptionHandler::GenerateDump cloned child 31
[err] ExceptionHandler::SendContinueSignalToChild sent continue signal to child
[err] ExceptionHandler::WaitForContinueSignal waiting for continue signal...
[err] Unable to init server: Could not connect: Connection refused
<process did exit: exitCode=null, signal=SIGSEGV>
============================================================
Note: use DEBUG=pw:api environment variable and rerun to capture Playwright logs.Error
at /function/node_modules/playwright/lib/server/firefox/ffConnection.js:54:63
at new Promise (<anonymous>)
at FFConnection.send (/function/node_modules/playwright/lib/server/firefox/ffConnection.js:53:16)
at Function.connect (/function/node_modules/playwright/lib/server/firefox/ffBrowser.js:44:24)
at Firefox._connectToTransport (/function/node_modules/playwright/lib/server/firefox/firefox.js:28:38)
at Firefox._innerLaunch (/function/node_modules/playwright/lib/server/browserType.js:90:36)
at async ProgressController.run (/function/node_modules/playwright/lib/server/progress.js:85:28)
at async Firefox.launch (/function/node_modules/playwright/lib/server/browserType.js:55:25)
at async BrowserTypeDispatcher.launch (/function/node_modules/playwright/lib/dispatchers/browserTypeDispatcher.js:30:25)
at async DispatcherConnection.dispatch (/function/node_modules/playwright/lib/dispatchers/dispatcher.js:147:28)
以我有限的知识,我推断这是与 Chromium 相同的多进程问题。不幸的是,Firefox 没有类似的启动参数来强制单一处理。因此,我还没有能够解决这个问题,恐怕它甚至不可能。
结论
最后,我设法让三分之二的浏览器在 AWS Lambda 上运行:WebKit 和 Chromium。当然有总比没有好,但 AWS Lambda 似乎是一个运行现代 Web 浏览器的艰难环境。但是,我计划继续测试,看看我是否在正确的现实生活用例中遇到更多问题。
所有代码都可以从我的 GitHub repoplaywright-aws-lambda-example中找到。第一个版本(标记:'v0.0.1')与此博客文章中显示的代码示例几乎相同。
我希望你喜欢阅读我的帖子并发现它很有用。我很高兴听到任何反馈,尤其是关于如何让 Firefox 运行 AWS Lambda 的想法。
更多推荐
所有评论(0)