☁️

miniflare環境のテストを日本語で絞れるようになりました

に公開

@cloudflare/vitest-pool-workers のリリース0.8.37より、テストケースを非ASCII文字でフィルタできるようになりました。

このようなテストスイートに対し……

describe('test sample', () => {
  test('/⁄*イ`^ᗜ^リ', () => {
    expect(1).toBe(1)
  })

  test('。。(dイロ-ロリ。。', () => {
    expect(1).toBe(2)
  })
})

引数 -t でパターンを渡した際、テストケースが正常にフィルタされるようになります。

npx vitest src/sample.test.ts -t '/⁄\*イ`^ᗜ^リ'
% npx vitest src/sample.test.ts -t '/⁄\*イ`^ᗜ^リ'

 DEV  v3.1.4 /path/to/repo

[vpw:info] Starting isolated runtimes for vitest.config.mjs...
 ✓ src/sample.test.ts (2 tests | 1 skipped) 9ms
   ✓ test sample > /⁄*イ`^ᗜ^リ 5ms
   ↓ test sample > 。。(dイロ-ロリ。。

 Test Files  1 passed (1)
      Tests  1 passed | 1 skipped (2)
   Start at  19:51:39
   Duration  457ms (transform 41ms, setup 105ms, collect 1ms, tests 9ms, environment 0ms, prepare 73ms)

以下、発生していた問題とその原因、解決策をまとめました。

概要

miniflare はCloudflare環境のシミュレータです[1]。Cloudflare Workersのアプリをwrangler dev で起動するとき、内部ではこの子が動いています。
KVやD1, Durable Objectsなど、Workersで使える様々な機能のシミュレーションをサポートしており、非常に便利です。

この miniflare に対してvitestとの統合を提供するパッケージが @cloudflare/vitest-pool-workers です[2]。これにより、テストケースをWorkersランタイム内で実行できるようになります。

問題

発生していた問題を整理します。

前提: テストスイートの命名

vitestのテストスイートには自由な名前をつけることができます。

describe('test sample', ()=>{
  test('test 1', ()=>{
    expect(1).toEqual(1)
  })
})

また、実行するテストケースは引数 -t でフィルタできます。

npx vitest /path/to/file.ts -t 'sample'

勿論、非ASCII文字を含むスイート名でも同様の操作が可能です。

describe('テストサンプル', ()=>{
  test('テストケース1', ()=>{
    expect(1).toEqual(1)
  })
})
npx vitest /path/to/file.ts -t 'サンプル'

テストをフィルタできない

しかしvitest-pool-workers を使ってテストを実行する場合、後者のようなテストをフィルタしようとすると……

TypeError: Cannot convert argument to a ByteString because the character at index 1404 has a value of 21336 which is greater than 255.
 ❯ webidl.converters.ByteString ../node_modules/undici/lib/fetch/webidl.js:431:13
 ❯ Object.record<ByteString, ByteString> ../node_modules/undici/lib/fetch/webidl.js:281:28
 ❯ webidl.converters.HeadersInit ../node_modules/undici/lib/fetch/headers.js:579:63
 ❯ Object.RequestInit ../node_modules/undici/lib/fetch/webidl.js:370:17
 ❯ new Request ../node_modules/undici/lib/fetch/request.js:54:30
 ❯ new _Request ../node_modules/@cloudflare/vitest-pool-workers/node_modules/miniflare/dist/src/index.js:1879:5
 ❯ #fetcherFetchCall ../node_modules/@cloudflare/vitest-pool-workers/node_modules/miniflare/dist/src/index.js:12025:21
 ❯ #call ../node_modules/@cloudflare/vitest-pool-workers/node_modules/miniflare/dist/src/index.js:11943:71
 ❯ Proxy.fetch ../node_modules/@cloudflare/vitest-pool-workers/node_modules/miniflare/dist/src/index.js:11933:34
 ❯ runTests ../node_modules/@cloudflare/vitest-pool-workers/dist/pool/index.mjs:1517:26

よくわからないエラーが出て実行できない……という問題がありました。

https://zenn.dev/enchan1207/scraps/2c3ef8c0fa5e0b

原因

この問題は、テストのコンテキストをワーカー(テストを実行する環境, ≠Workers)へ正しく受け渡せていないことが原因で発生していました。

コンテキストのやりとり

workers-sdk のコードを見ると、関数 runTests 内部でワーカーのコンテキストを MF-Vitest-Worker-Data というリクエストヘッダに乗せて送信している箇所があります。

https://github.com/cloudflare/workers-sdk/blob/9cee4b0c99cf8d2eeeaf647fb99757402635926a/packages/vitest-pool-workers/src/pool/index.ts#L769-L779

コンテキストはここで定義されています。737行目の config オブジェクトがメタ情報を保持しており、-t で与えたフィルタパターンは RegExp オブジェクトとして プロパティ testNamePattern に載っているようです(デバッガで確認)。

https://github.com/cloudflare/workers-sdk/blob/9cee4b0c99cf8d2eeeaf647fb99757402635926a/packages/vitest-pool-workers/src/pool/index.ts#L733-L744

生成されたコンテキストはdevalueによるシリアライズを経てリクエストヘッダに載り、ワーカーに送信されます。これを受け取ったワーカーは同じく devalue によってデシリアライズを行い、コンテキストを復元します。

シリアライズの問題

先述の通り、フィルタパターンは config.testNamePattern に格納されています。devalueのコードによれば、RegExp オブジェクトは ['RegExp', obj.source, obj.flags?] という型のtupleに変換されるようなので……

https://github.com/Rich-Harris/devalue/blob/f3fd2aa93d79f21746555671f955a897335edb1b/src/stringify.js#L93-L98

たとえば テスト.+ のようなパターンが testNamePattern に渡された場合、シリアライズ結果は以下のようになります。

[
  'RegExp',
  'テスト.+'
]

テストランナーがこれをそのままリクエストヘッダに載せようとし、しかしリクエストヘッダに非ASCII文字を含めることはできないため、エラーになっていた……ということのようです。

対応

コンテキストのシリアライズ時にパターン文字列をそのまま渡さなければよいので……

  • devalueのRegExpに対する処理に手を入れる
  • vitest-pool-workers側にカスタムのシリアライザを挟む

のいずれかで解決できそうです。

どちらのアプローチを取るかは迷ったのですが、devalueはデータをASCII文字列に変換するためのパッケージではないこと、またプレーンな string についてもエンコード等の処理は挟まっていなかったことなどから、後者を選択しました。

幸いにもカスタムのシリアライズ処理を挟む仕組みがdevalueから提供されており(ReducersRevivers)、vitest-pool-workers側でもすでに採用されているようだったので……
今回はここに RegExp 用のシリアライザを挟みました。

https://github.com/cloudflare/workers-sdk/blob/70ba9fbf905a9ba5fe158d0bc8d48f6bf31712a2/packages/miniflare/src/workers/core/devalue.ts#L63-L69

source をBase64でエンコードするだけの簡単なオーバライドです。なお flags にはアルファベットしか入らないため[3]、そのまま横流しにしています。

この修正(と、確認用のテストいくつか)をまとめてPRを提出、無事にapproveされました。
変更分を取り込んだバージョンが6月11日にリリースされたのをきっかけに、この記事を書いています。

https://github.com/cloudflare/workers-sdk/pull/9454

おわりに

ちいさなバグフィックスとはいえ、Cloudflareのソフトウェアに一つ貢献できたのは嬉しかったです。(この修正がパッケージの方針として合っているかどうかはちょっと分かりませんが……)

Cloudflare Workersは今非常にアツいので、今後も積極的に使っていきたいところです。

それでは。

脚注
  1. https://developers.cloudflare.com/workers/testing/miniflare ↩︎

  2. https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test ↩︎

  3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#advanced_searching_with_flags ↩︎

Discussion