【Golang】【wasm】テストの仕方と注意点 @ Go 1.16+


WebAssembly 用のアプリでもテストがしたい

Go 言語(以下 Golang)で組んだ WebAssembly(wasm)が動きはするものの、go test でテストを実行すると exec format errorimports syscall/js エラーが出る。

fork/exec /tmp/go-buildxxxxxxxxx/xxxx/my_pkg_hoge.test: exec format error
imports syscall/js: build constraints exclude all Go files in /usr/local/go/src/syscall/js
  • 検証環境: Go 1.15.6(golang:1.15-alpine), 1.16.5(tinygo/tinygo:latest), 1.16.6(golang:alpine)(いずれも Docker 環境)

TL; DR (今北産業)

  1. node.js をインストールする。
  2. 環境変数の PATHgo_js_wasm_exec のあるディレクトリを追加する。
    (一般的に ${GOPATH}/misc/wasm
  3. 実行時に OS とアーキテクチャを指定する。GOOS=js GOARCH=wasm go test など)

js_test.go | js | syscall | src @ golang.org より筆者訳)

TS; DR (Go で wasm のテストを完全に理解した気になっている俺様プラクティス)

  • WebAssembly 用のソースとはいえ、テストの書き方は通常でおk。
  • 問題は syscall/js パッケージ。js,wasm でビルドする必要がある。GOOS=js GOARCH=wasm go test など)
  • js,wasm でビルドだけでなく Node.js も必要。

Golang で WebAssembly 用の開発をする場合でもテストは行いたいものです。Golang 初心者ゆえ、テストを書くだけで最低限のことをしている気になれるので、安心できるのです。

しかし、ふいんき・・・・で Golang を触っているものだから、コンパイルしたものは動くものの、いつも通りの Golang のテストを書いてもエラーが出るのです。

後述するように、落ち着いて考えればわかることなのですが、Golang 初心者に輪をかけて、せっかちなものだから「(えっ?あっ!WebAssembly だからブラウザでテストしないといけないの?)」と焦ってしまいました。

さらに、ガバガバなくせに Go でのカバレッジ(テストの網羅率)が脳裏をよぎります。

3 歩ですぐ忘れるのですが、おそらく大事なのは wasm を提供する側でのテストと、それを利用する側でのテストは別物であるということです。つまり、Golang 側でのテストと、Javascript 側でのテストです。

この記事では(自分への戒めも含めて)Golang 側でのテストに注力し、気づいた点や学んだ点を反芻はんすうするためと、未来の自分のために残したいと思います。

テストが実行される流れを思い出す。ゆっくりと

Golang はコンパイル型の言語です。そのため、テストの場合でもソースを temp ディレクトリに作成ビルドして実行します。

このことは「知ってはいる」のですが、wasm という目新しい用語に翻弄されてテストできないと早とちりしてしまいました。

ポイントは「テストがビルドしたバイナリを『誰が』実行するのか」ということです。この「誰」とはアーキテクチャ(CPU)のことです。

環境変数が GOOS=darwin GOARCH=amd64 の場合は go test ./... すると、OS が macOS で CPU が AMD/Intel 64bit 向けのバイナリがビルドされ、テストが実行されます。

逆に macOS 上で GOOS=linux GOARCH=amd64 go test ./... すると exec format error が出ます。当然です。CPU は合っていても、OS が違うためです。

しかし、よく考えもせず「(あっ!となると wasm を作るときは GOOS=js GOARCH=wasm go build とするから、このマシン(Mac)ではテストできないんジャマイカ!)」となってしまったのです。

Golang で WebAssembly (wasm)を考える場合の 2 つのポイント

  1. syscall/js パッケージの存在
  2. wasm バイナリの実行者

Golang で WebAssembly 用の "Hello, world!" が動いたので喜んでいたのもつかの間、自分の既存のアプリを WebAssembly 対応させようとサイトを練り歩いたりサンプル・プログラムを見ていると syscall/js のパッケージを import しないといけないことに気づきます。

この syscall/js.go パッケージは、ブラウザの Javascript が wasm バイナリにアクセスした際のブリッジ(橋渡し)もしくはゲート(出入口)となるパッケージです。

Javascript の型と Golang 用の型を変換してくれたり、Javascript 側からのリクエスト(処理依頼)で Golang 側から POST された画像などを取得するなど、交換手のような仕組みを提供してくれています。

問題は syscall/js.go パッケージは GOOS=js GOARCH=wasm でないと動かないことです。

なぜなら、syscall/js.go パッケージのソース・コードのヘッダを見ると # +build js,wasm が指定されているためです。

  • js.go | js | syscall | src @ golang.org

ソースコードの package 宣言の前のコメント行に # +build という記載があった場合は、マッチした GOOSGOARCH の場合のみビルドの対象にすることができます。

これが、通常通り go test ./... と実行しても imports syscall/js エラーが出る原因です。GOOS/GOARCHjs/wasm ではないためです。

この仕組みは 1 つのリポジトリで複数 OS に対応したい場合に力を発揮します。特にシステム・コールがらみ(OS 独自の API やコマンドを利用)の場合です。

例えば、とある関数のコードを「Windows 用」「Linux 用」「macOS(darwin)用」と分けておき、各々のソースコードのヘッダに # +build darwin などと限定させることで、他のパッケージは同じ関数名を使うことができます。

🐒   この OS 互換について、以下のパッケージのリポジトリが、とても参考になります。ターミナル/コマンドプロンプトからの入力を OS ごとにわけることで一元化してくれる、CUI アプリを作る場合に、たいへんありがたいパッケージです。

ここで少し整理します。

  1. syscall/js パッケージを使う場合は GOOS=js GOARCH=wasm でテストやビルドを実行しないとパッケージ足らずでエラーになる。
  2. GOOS=js GOARCH=wasm でテストやビルドすると、GOOSGOARCH が実行環境と合わないためエラーになる。

この 2 つの矛盾を何とかしないといけません。

ビルドした wasm を Web サーバに設置した時のことを思い出す

Hello, world! をブラウザで動かした時のことを思い出しましょう。必要なファイルは以下ような感じだったと思います。

$ tree ./docs
./docs
├── index.html # <- 元 wasm_exec.html
├── test.wasm
└── wasm_exec.js

ここで重要なのが wasm_exec.js です。Javascript が *.wasm バイナリを読み込んで関数を実行するのに必要なものだからです。

つまり、Golang 側で言うところの syscall/js と似た役割を、Javascript 側では wasm_exec.jsになってくれているのです。

wasm_exec.js の注意点として、*.wasm を作った(ビルドした)コンパイラによって中身が変わることです。

つまり、コンパイラが変わった場合は、wasm_exec.js もコンパイラに合わせてたものに変えないといけません

これを読み違えたのか、ネットの記事では「公式リポジトリから最新版をダウンロードしろ」的なことも書いてあったりするのですが、その公式リポジトリの Wiki ではシンプルに「ローカルからコピーする」と書いてあります。

Copy the JavaScript support file:

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

WebAssembly | Wiki | go | golang @ GitHub より)

案の定、ネット記事をベースに試して、動かないので Issue を立てた人がいて、ドキュメント嫁から叱られてました。自分も叱られた思いがします。

I then copied the js / html fiels for running wasm in browser via:
https://github.com/golang/go/tree/master/misc/wasm

Do not copy from master branch. Use the files from your distribution as mentioned in the wiki page.

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . and same for the html file.
issue コメント | Issue #29827 | go | golang @ GitHub より)

以下、筆者訳。

次に、wasm を走らせる js と html ファイルをブラウザ経由で以下からピーコピーコしてきました。
https://github.com/golang/go/tree/master/misc/wasm

マスターブランチからコピペピピックしないでください。Wiki にもあるように、自分に配布された(Go にバンドルしてきた)ものを使ってください。html ファイルについても同様です。

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

つまり、wasm をコンパイルしたものが提供している wasm_exec.* を利用しないといけないということです。

例えば、Go の標準コンパイラより小さいバイナリが作成できる TinyGo コンパイラを使う場合も同様です。

この場合は、TinyGo と同じ環境にある Go が提供しているものではなく、実際にコンパイルする TinyGo が提供している wasm_exec.js を使います。具体的には、TinyGo の Docker の場合は以下のディレクトリにあります。

  • wasm_exec.js: /usr/local/tinygo/targets/wasm_exec.js
  • wasm_exec.html: /usr/local/tinygo/src/examples/wasm/main/index.html

さて、コンパイラに合った wasm_exec.js を使うことは理解できました。違うものだと Javascript がコンパイラの API を理解できないためです。

では、本題の「Golang 側で wasm バイナリをテストする」にはどうすればいいでしょう。

Node.js は OS みたいなもの

復習ですが、wasm のバイナリは OS が js(Javascript)、アーキテクチャが wasmビルドコンパイルされています。(GOOS=js GOARCH=wasm go build ./main.go -o ./doc/mywasm.wasm

そして、実行する時は Javascript 環境で wasm の構文を解釈できるプロセッサー(処理系統)でないといけません。つまり、Javascript + WebAssembly 対応のブラウザなら実行できるということです。

問題は「テストを実行する場合はどうなるのか」というところです。

まず、アーキテクチャの問題ですが、先の「wasm_exec.js はコンパイラと合ったものを使う」ことで Javascript 側から wasm を処理することができます。となると、残る問題は OS である js(Javascript)をどうするかです。

もぅ、すでにお気づきだと思いますが、Node.js を介して実行すればいいのです。

Node.js(node)は、Javascript のランタイムです。つまり、php ./index.phppython ./index.py のように、node ./index.js と実行すると Javascript を実行できるのです。

Golang のテスト(go test)の場合、まずテスト用にバイナリをビルドしてから、直接バイナリを実行して、その結果を取得することでテストを行います。

つまり、go$ /path/to/mybuiltapp_test ...(テスト用の引数)... のように実行するのです。

となると、この時に go$ node /path/to/mybuiltapp_test ...(テスト用の引数)... と実行出来れば、その実行結果をテストに反映できることになります。

何か見えてきたでしょうか。

そう、コンパイルは go にさせて、バイナリの実行は node にまかせればいいのです。

具体的には go run-exec オプションが使われます。

ここで、-exec オプションの詳細の前に確認したいことがあります。

先の「ドキュメント嫁に叱られる」件で「配布されたものを使え」と cp $(go env GOROOT)/misc/wasm/wasm_exec.js 光線を浴びました。そのディレクトリにあるファイルを見てみましょう。

$ ls -lah "$(go env GOROOT)/misc/wasm"
total 36K    
drwxr-xr-x    2 root     root        4.0K Dec  4  2020 .
drwxr-xr-x   12 root     root        4.0K Dec  4  2020 ..
-rwxr-xr-x    1 root     root         441 Dec  4  2020 go_js_wasm_exec
-rw-r--r--    1 root     root        1.3K Dec  4  2020 wasm_exec.html
-rw-r--r--    1 root     root       16.7K Dec  4  2020 wasm_exec.js

「変なところからコピペピピックせずに、ここからコピーしろ」と言われた wasm_exec.jswasm_exec.html が確認できます。

ここで注目してもらいたいのが go_js_wasm_exec ファイルです。初めて出て来た名前ですが、これが -exec に使われます。

Golang は、go run 実行時に -exec オプションが指定されていない・・・場合、GOOSGOARCH が現在の環境とマッチしているか否かで挙動が変わります。

マッチしている場合は、テスト用に作成されたバイナリはそのまま実行されます。

$ ./path/to/mybuiltapp_test ...(テスト用の引数)...

マッチしない(異なる)場合は go_<GOOS>_<GOARCH>_exec という書式のファイルを、パス(環境変数の PATH にあるディレクトリ)から探し、それを使ってビルドされたテスト用バイナリを実行します。

この時、go run -exec=go_<GOOS>_<GOARCH>_exec ./... として実行するのと同じ挙動になります。

つまり GOOS=js GOARCH=wasm の場合、ビルドコンパイル後、go_js_wasm_exec を探して以下のようにテスト用に作成されたバイナリを実行します。

$ go_js_wasm_exec /path/to/mybuiltapp_test ...(テスト用の引数)...

そして、go_js_wasm_exec の中身ですが、同ディレクトリにある wasm_exec.js を使った Javascript で書かれたテストを作成して node に渡し、その実行結果を Go が解釈できる形で返します。パーサーのような役割をします。

ここで大事なのが go_js_wasm_exec を見つけられるようにパスを通さないといけないということと、node がインストールされていないといけないことです。

まとめ

  1. HTML で使う wasm_exec.js は、コンパイラー(Go もしくは TinyGo)が提供しているものを使う。
  2. wasm_exec.js などがあったディレクトリを、OS の検索パス(環境変数 PATH など)に追加する。
  3. node コマンドが実行できるように Node.js を入れておくnpm などのパッケージ・マネージャーは基本的に不要)
  4. go test ./... する時に GOOS=js GOARCH=wasm go test ./... と OS とアーキテクチャを指定する。