☰
Wing 云服务(cloud.Service)在模拟器中的令牌(Token)绑定与端到端测试
2026/10/14 3:07:30 网站建设 项目流程
  • 编程语言
  • 编译器
  • 云原生
  • 语言运行时

【免费下载链接】wing

A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡

项目地址:https://gitcode.com/gh_mirrors/wi/wing
点击查看免费下载

cloud.Service是 Wing 标准库中用于定义长运行服务的核心资源,而“令牌(Token)”则是 Wing 在编译期无法解析、必须到运行时才能确定的值(例如 API 的 URL)。本文以 tools/hangar/snapshots/test_corpus/sdk_tests/service/tokens.test.w_test_sim.md 这份 Hangar 测试快照为核心线索,深入拆解 Wing 的 Service 资源如何在sim(模拟器)目标下自动“绑定并使用令牌”,并还原其背后cloud.Service、util.waitUntil、令牌解析机制与 Sandbox 沙箱进程的完整实现链路。

一、这份快照文档到底在记录什么

先看这份快照的完整原文:

# [tokens.test.w](https://link.gitcode.com/i/8edfcd4c0a5d1b33a81f7d15f21488bd) | test | sim ## stdout.log ```log pass ─ tokens.test.wsim » root/Default/test:will bind and use tokens Tests 1 passed (1) Snapshots 1 skipped Test Files 1 passed (1) Duration <DURATION>
它由三部分构成: 1. **标题行**:`tokens.test.w | test | sim`,指明被测源文件是仓库中的 [tests/sdk_tests/service/tokens.test.w](https://link.gitcode.com/i/08063e2d2c1326fafffc4e967c0c663a),执行方式是 `wing test`,目标平台是 `sim`。 2. **stdout.log 代码块**:记录了该测试在模拟器中的标准输出。核心一行是 `pass ─ tokens.test.wsim » root/Default/test:will bind and use tokens`,表示名为 “will bind and use tokens” 的测试用例**通过(pass)**。 3. **统计信息**:`Tests 1 passed (1)`、`Snapshots 1 skipped`、`Test Files 1 passed (1)`、`Duration <DURATION>`。其中 `Duration` 被替换为占位符 `<DURATION>`,这是 Hangar 快照“规范化(sanitize)”机制的产物。 ### 快照的生成机制 这份快照不是手写的,而是由 [tools/hangar/src/utils.ts](https://link.gitcode.com/i/5856c7ff44000d5500ede71577b634df) 中的 `createMarkdownSnapshot()` 函数自动生成的: ```ts const snapPath = join( snapshotDir, "test_corpus", dirname(relativePath), `${wingFile}_${testCase}_${target}.md` ); let md = `# ${wingFile}}/${wingFile}) | ${testCase} | ${target}\n\n`;

它会把运行结果按test_corpus/<目录>/<文件名>_<用例>_<目标>.md的约定落到快照目录,并在标题中内嵌指向源码文件的相对链接——这就是快照第一行../../../../../../tests/sdk_tests/service/tokens.test.w的来源。由于快照生成时会对输出做规范化处理(见下),真正的测试文件位于tests/sdk_tests/service/目录,读者应前往该目录查看被测代码原文。

<DURATION>占位符与输出的确定性

Hangar 为了保证快照跨平台、跨机器稳定可比,在 sanitizeOutput() 中做了一系列输出清洗:

  • 统一换行符、把 Windows 反斜杠替换为正斜杠;
  • 去掉 Rust 源码中的行列号(src/xxx.rs:LINE:COL)、绝对堆栈路径;
  • 把Duration 1m12.3s这类耗时统一替换为Duration <DURATION>;
  • 端口号、IP 地址、时间戳等易变内容分别替换为<PORT>、<IP>:<PORT>、<TIMESTAMP>。

这就是快照中Duration <DURATION>的由来:只有耗时被抹平,测试断言本身(pass/失败)才具备稳定的可比较价值。

二、被测源码:Service 如何“绑定并使用令牌”

快照指向的被测文件是 tests/sdk_tests/service/tokens.test.w,其完整内容如下:

bring cloud; bring util; bring http; // only supported in the "sim" target for now if @target == "sim" { let b = new cloud.Bucket(); let api = new cloud.Api(); api.get("/", inflight () => { return { body: "api test", status: 200 }; }); let s = new cloud.Service(inflight () => { let result = http.get(api.url); b.put("service.txt", result.body); }); test "will bind and use tokens" { util.waitUntil(inflight () => { return b.exists("service.txt"); }); assert(b.get("service.txt") == "api test"); } }

这段代码包含了三个互相咬合的关键点:

  1. @target == "sim"条件编译:文件开头就明确注明“目前仅sim目标支持(only supported in the sim target for now)”,所以整个测试体被包在目标判断里,只有以-t sim运行时才会生效。
  2. Service 的 inflight handler 中使用api.url:api.url是一个典型的令牌(token)——API 的真实地址只有在模拟器启动、端口分配之后才能确定,编译期无法知晓。Service 在启动时执行http.get(api.url)并把响应体写入 Bucket。
  3. 测试用例本身不直接“驱动” Service:test "will bind and use tokens"只是用util.waitUntil轮询 Bucket 里是否出现了service.txt,一旦出现就断言其内容等于"api test"。Service 是自动启动(autoStart 默认 true)并在后台运行的,测试靠“轮询结果”来验证整条链路是否打通。

这正是测试名 “will bind and use tokens” 的含义:Service 的 inflight 代码“绑定”了api.url这个令牌,并在运行时“使用”它成功发起了 HTTP 请求。

为什么要用 waitUntil 轮询而不是直接断言

因为 Service 的 handler 是异步、后台执行的,测试无法预知它何时完成第一次 HTTP 请求与 Bucket 写入。util.waitUntil提供了确定性的等待语义。它的实现在 packages/@winglang/sdk/src/util/util.ts 的Util.waitUntil静态方法中:

public static async waitUntil( predicate: IPredicateHandler, props: WaitUntilProps = {}, ): Promise<boolean> { const timeout = props.timeout ?? Duration.fromMinutes(1); const interval = props.interval ?? Duration.fromSeconds(0.1); const f = predicate as any; let elapsed = 0; while (elapsed < timeout.seconds) { if (await f()) { return true; } elapsed += interval.seconds; await this.sleep(interval); } if (props.throws !== false) { throw new Error("Timeout elapsed"); } return false; }

其行为要点(对应 WaitUntilProps 接口):

参数默认值说明
timeout1 分钟轮询总超时,超过后抛出Timeout elapsed错误
interval0.1 秒两次轮询之间的间隔
throwstrue超时后是否抛错;传false则改为返回布尔值

实现上注意:超时统计只累加间隔时长,不把谓词本身的执行耗时计入(源码注释明确提到这一点),因此谓词应保持轻量。测试中谓词b.exists("service.txt")刚好是这种轻量检查。

三、令牌机制:编译期无法解析的值,运行时才落地

“绑定并使用令牌”的底层支撑是 Wing SDK 的令牌解析器(Token Resolver)体系,定义在 packages/@winglang/sdk/src/core/tokens.ts:

export interface ITokenResolver { isToken(value: any): boolean; // 判断一个值是否为令牌 lift(value: any): string; // 把一个令牌提升到 inflight 上下文 onLiftValue(host: IInflightHost, value: any): void; // 把令牌绑定到宿主(宿主即 Function/Service 等 inflight host) }

其设计思路(源码注释原话):“Tokens values are captured as environment variable, and resolved through the compilation target token mechanism.”——令牌值会被“捕获”为环境变量,再通过各编译目标的令牌机制解析。

环境变量名由 tokenEnvName() 生成:

export function tokenEnvName(value: string): string { return `WING_TOKEN_${value .replace(/([^a-zA-Z0-9]+)/g, "_") .replace(/_+$/, "") .replace(/^_+/, "") .toUpperCase()}`; }

即把令牌字符串中非字母数字的字符替换为下划线并转大写,前缀WING_TOKEN_。

sim 目标如何解析令牌

针对本快照使用的sim目标,令牌解析器实现在 packages/@winglang/sdk/src/target-sim/tokens.ts 的SimTokens类中:

  • isToken:对字符串值用SIMULATOR_TOKEN_REGEX正则判断是否是模拟器令牌;
  • lift:把令牌提升为运行时表达式process.env["WING_TOKEN_..."],即从进程环境变量读取;
  • onLiftValue:把令牌以WING_TOKEN_<规范化名>为键写入宿主的addEnvironment,让 inflight 代码在子进程里能通过环境变量拿到真实值。

对照而言,Terraform 类目标(如tf-aws)在 packages/@winglang/sdk/src/shared-tf/tokens.ts 中走另一条路:令牌本质是 CDKTF 的未解析 Token,lift生成JSON.parse(process.env["WING_TOKEN_..."]),onLiftValue则用Fn.jsonencode(value)把值编码成环境变量注入宿主(当前实现限定宿主必须是Function)。这正好呼应了测试文件里那句“目前仅 sim 目标支持”的注释——令牌绑定到 Service 宿主的路径在不同目标下能力并不一致。

四、Service 的绑定链路:从 inflight 代码到模拟器沙箱

抽象基类 cloud.Service

packages/@winglang/sdk/src/cloud/service.ts 定义了cloud.Service的抽象契约:

  • 构造签名:new cloud.Service(handler: IServiceHandler, props?: ServiceProps);
  • ServiceProps两个可选字段:env(环境变量,默认无)与autoStart(是否自动启动,默认true)——这正是测试用例里 Service 后台自动运行的来源;
  • inflight 接口IServiceClient提供三个方法:start()、stop()、started()。

抽象基类的_preSynthesize()(service.ts#L113-L144)会生成服务入口代码并写入workdir下的<assetName>.cjs文件,同时执行关键的一步:

Lifting.lift(this.handler, this, ["handle"]);

即把 handler 的 inflight “handle” 方法提升到 Service 宿主上——测试中api.url令牌之所以能被“绑定”到 Service,就是通过这套 lift 机制把令牌值写入 Service 的环境变量。

模拟器目标 Service

packages/@winglang/sdk/src/target-sim/service.ts 是 sim 目标下的具体实现,它把绑定结果转成模拟器资源描述:

const props: ServiceSchema = { environmentVariables: this.env, sourceCodeFile: relative(App.of(this).outdir, this.entrypoint), autoStart: this.autoStart, }; return { type: cloud.SERVICE_FQN, props, policy, };

environmentVariables正是SimTokens.onLiftValue写入的那些WING_TOKEN_*环境变量,schema 定义见 target-sim/schema-resources.ts#L78-L85。

模拟器运行时:Sandbox 子进程

packages/@winglang/sdk/src/target-sim/service.inflight.ts 负责真正的运行时行为:

  • init():若autoStart为 true,则立刻调用start()——这是测试中 Service 在后台自动跑起来的直接原因;
  • start():创建Sandbox子进程(packages/@winglang/sdk/src/shared/sandbox.ts),并把环境变量连同WING_SIMULATOR_URL、WING_SIMULATOR_CALLER注入子进程,然后调用沙箱的start入口;
  • stop()/started():对应停止与状态查询;
  • cleanup():测试结束、模拟器回收资源时调用stop()做清理。

Sandbox 的核心机制(sandbox.ts#L218-L323 的call())是:父进程通过child_process.fork()拉起一个 Node 子进程,把{ fn, args }消息发过去,子进程执行exportsfn后回传{ type: "ok", value }或{ type: "error", reason };若调用超时(默认无超时,除非显式传入timeout)或进程异常退出,则杀死子进程并拒绝 Promise。

于是整条链路闭环了:

api.url(编译期令牌) └─ SimTokens.onLiftValue → WING_TOKEN_* 环境变量写入 Service └─ Service.toSimulator → ServiceSchema.environmentVariables └─ service.inflight.ts start() → Sandbox 子进程注入 env └─ inflight handler 中 http.get(process.env["WING_TOKEN_..."]) └─ 请求模拟器内 API → 写入 Bucket → waitUntil 轮询命中 → 断言通过

五、从快照回推 Hangar 测试运行器

Hangar(tools/hangar)是 Wing 仓库的测试运行器,它把tests/sdk_tests/**/*.test.w等语料跑在多个目标上并生成上述快照。以错误用例为例,tools/hangar/src/error.test.ts 展示了通用运行模式:

const platforms = ["sim"]; const args = ["test"]; const out = await runWingCommand({ cwd: tmpDir, wingFile: relativeWingFile, platforms, args, expectFailure: true, });

底层通过 runWingCommand() 调用 wing CLI:

const out = await execa( wingBin, [ "--no-update-check", "--no-analytics", ...options.args, // 即 ["test"] options.wingFile ?? "", // 被测 .w 文件 ...platformOptions, // 即 ["-t", "sim"] ], ... );

也就是说,执行tokens.test.w的快照对应命令等价于:

wing --no-update-check --no-analytics test tests/sdk_tests/service/tokens.test.w -t sim

而Test Files 1 passed (1)这类汇总行,就是wing test在模拟器目标下的测试框架输出;Snapshots 1 skipped表示快照比对跳过(由 Hangar 的快照策略控制),Tests 1 passed (1)则确认用例本身全部通过。读者如需复现,可在本仓库根目录安装依赖后对上述wing命令自行验证(命令基于 utils.ts 中wingBin解析出的本地 CLI 路径)。

六、可迁移的实战要点

这份快照连同其测试代码,为在 Wing 中编写“Service 异步执行 + 令牌依赖”的用例提供了可直接迁移的范式:

  1. 令牌依赖要放到运行时验证:像api.url、资源 ARN 这类编译期未知的值,正确姿势是让 inflight 代码“绑定”它,再用端到端断言验证运行时行为——测试名 “will bind and use tokens” 本身就是这个意图的注释。
  2. 异步结果用util.waitUntil收敛:后台进程的产物(本例是 Bucket 文件)不应靠固定sleep等待,而应轮询直到出现或超时;记得throws默认会在超时抛错,适合测试环境直接使用。
  3. 目标差异要显式声明:测试文件用if @target == "sim"圈定适用范围,并注释说明当前能力边界(令牌绑定到 Service 目前仅 sim 目标支持),这对多目标仓库的用例组织是一个很好的参照。
  4. 快照输出应可复现:任何 CI 化的快照都应像 Hangar 一样抹平耗时、端口、绝对路径等易变信息(<DURATION>、<PORT>、<ABSOLUTE>),否则快照会因机器差异频繁误报。

需要进一步研究时,建议按以下路径深入仓库:

  • 被测源码:tests/sdk_tests/service/tokens.test.w,同目录另有 callbacks.test.w、http-server.test.w 等 Service 相关用例可对比;
  • Service 抽象契约与 API 文档:cloud/service.ts、docs/api/04-standard-library/cloud/service.md;
  • sim 目标实现与沙箱:target-sim/service.inflight.ts、shared/sandbox.ts;
  • 令牌解析器:core/tokens.ts、target-sim/tokens.ts、shared-tf/tokens.ts;
  • 快照生成与运行器:tools/hangar/src/utils.ts、tools/hangar/src/error.test.ts。
  • 编程语言
  • 编译器
  • 云原生
  • 语言运行时

【免费下载链接】wing

A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡

项目地址:https://gitcode.com/gh_mirrors/wi/wing
点击查看免费下载

相关推荐

上一篇:精通 React 中的 Material UI Select:从基础用法到表单集成与高级定制的完整实战指南
下一篇:使用 `dyn Trait` 在 Rust 中构建异构数据集合:comprehensive-rust 动态分发实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询