スキマ時間は意外とある

昨年の暮れに12インチMacbookを入手してからというもの、肌身離さず12インチMacbookを持ち歩くようになった。

yusukeiwaki.hatenablog.com

こいつはとても優秀で、本当にどこにいてもコーディングが可能なのだ。スタバでドヤるという意味ではなく、エレベータの待ち時間とか、横断歩道の長い信号待ちとか、車の渋滞で停車中とか。

puppeteer-rubyやplaywright-ruby-clientの開発が少しずつながら確実に進捗している。

訳あって、3月に入ってからは本当にまとまった時間がとれないので、スキマ時間を積極的に活用している。1つ1つは10秒から1分程度だが、その間に1つでも2つでも単体試験を書けば、OSS開発はかなり捗る。

今後も(物理的に)事故らない程度に続けていこう。

第13回フクオカRuby大賞で入賞!その振り返り。

第13回フクオカruby大賞っていう、Rubyをネタにしたビジネスコンテストてきなものに参加した。

www.digitalfukuoka.jp

まだどこにも公表されていないし、開催から数日が経過して自分でも忘れつつあるんだけどw 、優秀賞とマネーフォワード賞の2つをいただけた。(わーい!)

(2021.02.09 公開されました!! <結果発表>「第13回フクオカRuby大賞」審査結果 - 福岡県Ruby・コンテンツビジネス振興会議 )

なぜ参加した?

puppeteer-ruby を趣味の開発でかれこれ半年以上かけてつくっていたけど、一発どこかで披露したいなと思ったのが事の発端。

puppeteer-rubyはビジネス価値を求めて作っていたわけでは全くないのだが、いっぽうで自動試験の運用を段階的に変えていける可能性があるとも思っていた。

ちょうど会社でも自動試験の担当者が運用で苦しんでいて、空き時間で自動試験の段階移行をするための試作も重ねていたし、puppeteer-rubyがPoC(proof of concept)としては十分すぎるレベルに達している自覚はあった。

また、フクオカRuby大賞はビジネスコンテストな側面もありつつ、過去の入賞作品を見ると、

「falcon」 Samuel Williams(海外:ニュージーランド

とても見覚えのあるものがあった。(puppeteer-rubyの開発初期に、非同期処理をどうするか悩んでたときに見た socketry/async の作者!)

falconが世の中で使われているのは正直みたことはないのだけどw、それでも入賞しているというところでピンときた。

要は、作ったもの単体が価値があるものであるというよりも、その周辺技術や取り組みなどふくめて評価されているんじゃないかと。だとすると、puppeteer-rubyもそれ単体は試作+αくらいのレベルだが、総合力でいいところまでいけるんじゃないか?と。

ともあれ、目的意識としては「puppeteer-rubyを知ってもらえるだけでもいいな」くらいで気楽に参加することにした。

puppeteer-rubyの優位性は何か?

一応はビジネスコンテストっぽい感じなので、ビジネス価値を落ち着いて考えることにした。

  • Capybaraとの共存
  • Rubyでブラウザの自動操作をしようとしたときに、気軽に(Seleniumみたいにドライバーを別途用意するなど煩わしい手順なく)遊べる
  • RubyからCDP(Chrome DevTools)を使ってブラウザを自動操作できるライブラリはFerrumなどがあるが、それらはみなChromeしか自動操作できない。Firefoxも自動操作できるのは世界でpuppeteer-rubyだけ

候補はこのあたりだ。今回は 周辺技術も含めてしっかり価値が伝わるか? がポイントなので、もうすこし深堀りして考えた。

Capybaraとの共存

そもそもRubyRailsで自動試験を運用してる人はおるんか?という問い。

  • てすらぼってイベントに参加していろいろ聞いたが、Railsの関係者はおそらくいなかった。
    • ただSeleniumでしんどい思いをしていた人は結構いた。
  • そもそもE2Eテストは単体テストありきでオマケであるべき。
    • これからRailsで新規にサービス作る人がE2Eで苦しむことはほぼない。
  • RailsにSystemTestCase(実際にブラウザを動かすテスト)が導入されたのは5.1なので割と最近。これはなんのためのものだったんだろう?
    • 従来よりあったFeature specはそもそもCapybaraベースなので、Flakyテストしんどい問題を抱えていたはず。
      • 従来のFeature specでは、ActiveRecordのコネクション共有がされていないため、特定の状況を作り出すことができない。つまり、テストの単位を小さくすることが難しい。
      • SystemTestCaseであればFactoryBotを使って特定の状況を作り出すことは簡単。テストの単位を細かくすることで、Flakyテストが仮に発生しても単純リトライすればいいだけになる。
    • おそらくSystemTestCaseは、受け入れテストを書きたい人ではなく、すでに大規模なサービスを作ってしまった組織をメインターゲットにしていそう。

→puppeteer-rubyも、ターゲットを「すでに(あまり単体テストを書かないまま)大規模サービスを作ってしまった組織が、E2Eテストを運用している」前提にすることにした。そうすれば、従来の仕組みでは、Capybaraで苦しむかJSベースのテストフレームワークに載せ替えるかの2択しかない。それに対して、puppeteer-rubyは"補強・段階移行"を可能にする第三の選択肢を与えることができる。

気軽に遊べる

「ブラウザオートメーションで業務効率化♪」「R!P!A!」みたいな人は居るには居るかもしれない。ただ、そこについてビジネス価値創出は・・・あまりなさそう。

そもそも単発で適当に動けばいいレベルなら、Ferrumがすでにある。また、もしも"Puppeteerを使って"遊びたいのだとすると、それはRubyじゃなくてJavaScriptを選択したほうが賢明だ。

つまり、この点においてはpuppeteer-rubyの優位性はおそらく無い。

RubyからCDPでFirefoxを操作できる世界で唯一のライブラリ

「世界で唯一の」という文言はとても見栄えする。ただ、「Chromeで動かなくてFirefoxでは動く」みたいな謎めいたWebサービスでも扱わない限り、その優位性はない。

ちょうど2020年の春にIEサヨナラ&ChromiumベースEdgeが出て、今後ほとんどのサイトはChromeで動くことが必須になるだろう。そう考えると、「Firefoxを動かせる」ことは主張として弱い。

とりあえず応募してから考えることにした

応募文面としては、RubyRailsのE2Eテストの改善というところになるべく絞って書いた。

f:id:YusukeIwaki:20210124152130p:plain f:id:YusukeIwaki:20210124152200p:plain f:id:YusukeIwaki:20210124152233p:plain


ちなみに、この応募はWebのフォームではなく、Wordファイルを編集してメールで送る 必要がある。運営の方々に頑張っていただいているところ申し訳ないのだけど、これは言わずにはいられなかったw↓

ちなみに、応募後のやりとりもメールベースで、重要なファイルはPPAP(パスワードはあとでおくりますプロトコル)でやりとりされる。そこそこちゃんと届くメールアドレスが必須だ。

この時点では、プレゼン資料は未作成。puppeteer-rubyが実は動かない状態だったとしても、ただただ理想的なストーリーを書いて出すだけである。(「実は動かない状態だった」というのはマジな話で、プレゼン資料を作るときに気づいて直した↓)

本審査への招待がきたが、プレゼン資料は締切の2日前から作り始めた

12/21にこんなメールが来た。

f:id:YusukeIwaki:20210124152918p:plain f:id:YusukeIwaki:20210124153012p:plain

今年は新型コロナウィルス感染拡大防止のため、オンライン開催。プレゼン資料は年末年始をはさんで1/12までに作ってね、と。

メールを受け取ったときには、年末年始でプレゼン資料を作るつもりでいたが、実際にはそんなことはなく、締切2日前から焦って作った。年末年始は何をしていたかといえば、読書だw

yusukeiwaki.hatenablog.com

自分は、楽しいことは楽しいと思えるときにやらないと、一生やらない性格なので、こういう気に入ったの本は一気に読まないとだめなのだ。

そして読書が終わり、プレゼン資料を作るのかと思いきや、こんどは playwrightの仕組みを調べたくなって、Rubyクライアントを作ったりしていたw

yusukeiwaki.hatenablog.com

とことん遊び呆けた結果、プレゼン資料を作り始めたのが1/10だった、と。

かなりアホではあるけど、これはこれで有意義だったところもある。学習する組織の最初の方にある"システム思考"というのを使って、レガシー開発におけるE2Eテストの効果/レバレッジをシステム思考で考えてみることはできたし、playwright-ruby-clientを作る過程でPlaywrightの中身をソースコードレベルで深く読んだおかげで、PuppeteerとPlaywrightがそれぞれどういう強みを持っているか理解することができた。単純にpuppeteer-rubyのスライドを唸りながら作るのに比べて、周辺技術や背景をしっかり理解してストーリーを作ることができたと思う。

スライドはGoogle Slideで作った。これは本当に大失敗

Keynoteで作ると、スマホで移動中に編集することができないし」と思ってGoogle Slideでプレゼン資料を作った。

f:id:YusukeIwaki:20210124154053p:plain

が、結果的にはこれは大失敗だった。

  • プレゼン資料の提出がメールベースなので、PDFに変換する必要があるのだが、PDF変換するとレイアウト崩れを起こす
  • 本番はMicrosoft Teamsでのオンライン会議だったが、 Microsoft TeamsはGoogle Slideの発表画面を共有することができない (これが致命的)
  • 発表後にSpeakerDeckなどに資料を上げるにも、PDF変換すると(以下略...

そもそも「外出中に編集できるように」なんて考える必要はなかった。自分はショッピングモールの出口の渋滞中にもRSpecを書いてるくらい、どこにでも12インチMacbookを持ち歩いている。外出中にKeynoteを編集することくらい余裕だったはずだ。

今後はやはりKeynoteにしようと思う。

ちなみに、Google Slideをなるべくレイアウト崩れせずにSpeakerDeckに上げることは一応できた。Google SlideをMicrosoft PowerPoint(.pptx)でエクスポートして、それをPDF変換するといい。一部のフォントは変わってしまっているが、レイアウト崩れまでは起きなかった。

speakerdeck.com

発表練習しまくった

フクオカRuby大賞は過去にどんな作品が入賞しているのかは書いてあるが、どんな発表をしたのかという情報が一切ない。

エンジニア相手の勉強会のように、前提知識もりもりで気楽に話せる感じではないとして、一体どのくらいまで前提知識を期待してよいものか?

とりあえずよくわからないかったので、1から10まで説明する感じのプレゼンをまずは作ってみた。

発表時間10分に対して、22分10秒wwww

少し中身を削って発表すると・・・?

発表時間10分に対して、10分15秒wwwwww

・・・とはいえ、早口で話すのは非常に苦手なので、最終的には 重点的に説明するスライドを5つだけに絞って、あとはさらさらっと流す って感じにした。

練習は、Microsoft Teamsの会議録音機能を使って、自分で発表したのを自分で見返して文句をつける感じでブラッシュアップした。

f:id:YusukeIwaki:20210124155906p:plain

いざ本番

Teamsオンライン会議は、会社で使って慣れていたので、操作に手間取ることはなく、内容の説明も割とリラックスして発表できたかなと思う。

TeamsはZOOMと違って、プレゼン(画面共有)中に他の人の反応を一切見ることができないので、ひたすら画面を見てしゃべり続けることになる。時間計測用のタイマー(スマホのアラームアプリ)をパソコンの横において、時間どおりに発表できているかというところだけ注意して発表を進めた。(たぶん発表時間は少しオーバーしてる)

入賞!

審査結果発表は、おもいっきり業務時間中の夕方に行われる。今回は趣味のプロダクト開発で出ているので、業務中に参加するのも気が引けて、とりあえずiPadからTemas会議に入って、様子は見つつも仕事をしていた。(ごめんなさい)

ただ、なんかpuppeteer-rubyってのが聞こえた気が?!ってなって、急遽、上司に事情を話して、パソコンからTeamsに入り審査会に参加。(TeamsはiPadで入っている会議にそのままPCからもサラッと主催者/参加者に気づかれず入れるのは便利ねw)

マネーフォワードの方からのありがたいコメントを頂戴し、その後の優秀賞の発表後のコメントも、かなりアセアセしながら応えたw

持続可能な開発をしていかないと〜

今回、まさか入賞するなんて思っていなかったので、実はpuppeteer-rubyは手を抜き始めていた。

ただ、今回の発表で改めて感じたのは、自分がやってきた開発は思ったよりも需要はありそうで、もっと広めていくべきだということ。そうなると、趣味で1人で開発していくのには限界があって、 コミュニティとして開発できるようにpuppeteer-rubyを育てていかないといけない

まだ全然知名度はないと思うので、今後しばらくはpuppeteer-rubyだったりplaywright-ruby-clientだったりを作りつつも、Rubyでのブラウザオートメーションの布教活動っぽいことをやっていこうかなと思う。

(蛇足)みんなPuppeteerとPlaywrightを同一視しているけど・・・

PuppeteerとPlaywrightをどちらもソースコード読んだ身として、一言いっておこう。

  • Puppeteerは、すでに起動済みのChromeに対してアタッチする Puppeteer#connect というメソッドがある。
  • Playwrightは、Puppeteerから分化した直後はその機能があったが、サーバー・クライアントアーキテクチャが採用されたあたりで、その機能が完全になくなった(ref: feat(launch): introduce client, server & persistent launch modes (1) by pavelfeldman · Pull Request #838 · microsoft/playwright · GitHub )。Playwrightは、自身が起動したブラウザを操作することだけできる。
    • サーバークライアントモデルなので、「リモートサーバーにPlaywrightサーバーを立てて、そこあるブラウザをPlaywrightクライアントから操作する」みたいなことはおそらくできるが、 Playwright以外によって起動されたブラウザを操作することができない という意味。

Puppeteerは単純にCDPを使うことに特化したツールキットなのに対し、Playwrightはブラウザオートメーションのオールインワンツールキットという感じだ。

ほとんどのケースではPlaywrightのほうが有力なのは間違いない。しかし、今回のpuppeteer-rubyが対象としたような「すでにCapybaraでテストケースを運用していて、共存・段階移行したい」というケースでは、Capybaraが起動したブラウザにアタッチできるPuppeteerが必要である。Playwrightではこれができない。

なので、playwright-ruby-clientを作っているから、puppeteer-rubyはもうオワコン?とはならない。用途が全然違う。

playwright-ruby-clientはどちらかというと、Ferrumに近い位置づけで、今後はCupriteのようなCapybaraドライバを作ったり、Vesselのようなクローラー書く仕組みを作ったり、みたいな方向性で成長させていく必要がある。

puppeteer-rubyはあくまでCapybaraとの「共存」ができることを強みに、(本家Puppeteerがなくならない限りはw)育てていくだろう。

ブラウザ自動操作のPlaywrightはRubyからでも使える?

Playwrightのリリースノートに、気になる記載がある。

f:id:YusukeIwaki:20210112235602p:plain

Release v1.4.0 · microsoft/playwright · GitHub

英語が苦手でも読めるよう、みんな大好きdeepl翻訳をかけておこう。

f:id:YusukeIwaki:20210113011154p:plain

爆速の多言語展開スピードを支える何かがあるらしいことはわかる。

ただ、具体的にどういう仕組みで動いているとか、どうやればクライアントが実装できるよとか、そういう情報はドキュメントには一切書かれていない。 さすがマイクロソフト なので、いろいろソースを読んで調べた&実際にRubyからPlaywrightを使うPoCを作ってみた。

simple_scraping

PlaywrightはServer/Clientモジュールに分かれている

結論から言うと、

  • microsoft/playwright (TypeScript版)にはServerモジュールとClientモジュールの両方が含まれている
    • ただ、READMEなどに書かれている動作では、サーバー・クライアント動作は特にせず、1つのプロセスで自動化が動く
  • microsoft/playwright-python, microsoft/playwright-java, microsoft/playwright-sharp, mxschmitt/playwright-go はいずれも、PlaywrightのClientモジュールである

サーバーモジュールというのは何かというと、Playwright自身が特定のWebSocketまたはパイプ(標準入力/標準出力)をバインドして、そこで受けた命令をそっくりそのままChromeFirefoxSafariに(いい感じに変換して)投げる君。

クライアントモジュールというのは、ユーザが書くスクリプトで実際に使われるPageとかBrowserとかElementHandleとかそのへんのクラスで、サーバーに対して、WebSocketなりパイプなりで、要求を投げる君。

f:id:YusukeIwaki:20210208002728p:plain

PythonJavaC#やGoのクライアントは、内部的にplaywright-cliを使ってサーバーを起動している

リリースノートに、「Nodeじゃなくても」動かせるようにした、と書いてあった部分の話。

In the last release, we introduced an internal protocol to support Playwright in the none-Node environments

タイトルでネタバレしてしまったが、それぞれのクライアントには

  • playwright-cli (v1.8以降はplaywright driver)をダウンロードする部分
  • playwright-cli run-driver を実行する部分

がある。

Pythonであれば

Goであれば

試しに、手元で npx playwright-cli run-driver してみたら、なんとそれだけでPlaywrightサーバーが立ち上がるのがわかる。

ただ、playwright-pythonやplaywright-javaなどでダウンロードされているplaywright-cliはNode環境がなくても動くようにシングルバイナリ?っぽい形で配布されたもののようだ。 playwright-pythonjavaがダウンロードしているURLは https://playwright.azureedge.net/builds/cli/next/playwright-cli-0.180.0-next.1608746109749-cbc13bd-mac.zip こんな感じのもので、実際にそこからダウンロードして中身を見てみた↓

f:id:YusukeIwaki:20210113001801p:plain

playwright-cli run-driverはPlaywrightサーバーモジュールを起動するだけ!

playwright-cli run-driverの内部実装も一応メモっておく。

実に単純で、

// Implement driver command.
if (process.argv[2] === 'run-driver')
  runServer();
else if (process.argv[2] === 'print-api-json')
  printApiJson();

playwright-cli/cli.ts at v0.171.0 · microsoft/playwright-cli · GitHub

const { Playwright } = require('playwright/lib/server/playwright');

  (中略)

export function runServer() {
  installDebugController();
  installTracer();

  const dispatcherConnection = new DispatcherConnection();
  const transport = new Transport(process.stdout, process.stdin);
  transport.onclose = async () => {
    // Force exit after 30 seconds.
    setTimeout(() => process.exit(0), 30000);
    // Meanwhile, try to gracefully close all browsers.
    await gracefullyCloseAll();
    process.exit(0);
  };
  transport.onmessage = (message: string) => dispatcherConnection.dispatch(JSON.parse(message));
  dispatcherConnection.onmessage = (message: string) => transport.send(JSON.stringify(message));

  const playwright = new Playwright(__dirname, require('playwright/browsers.json')['browsers']);
  (playwright as any).electron = new Electron();
  new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
}

Playwrightサーバーモジュールを直接インクルードして起動し、通信路としてパイプ(stdin/stdout)を指定しているだけ。

あとは、Playwrightプロトコルをしゃべるクライアントを書けばいい

サーバーの立ち上げ方がわかったところで、あとは標準入力/標準出力を介してPlaywrightのプロトコルをしゃべるクライアントを書けばいいだけ、ということになる。

ただ、playwright-pythonもplaywright-javaもソースを見てみるとちょっと変わった作りをしていて、プロトコルJSONをもとにAPIクライアントインターフェースを自動生成するようになっている。

npx playwright-cli print-api-json | jq .

こうすると、どっっばーーーーーっとAPIインターフェースを定義したJSONが降ってくる。playwright-pythonもplaywright-javaもこれを頑張って解析してAPIクライアントインターフェースを生成している。

わかりやすいのはPythonで、 このへんでソース生成君がいて、実際に生成されたソースは

このあたりだ。

これとは別に、インターフェースの実装部分を、作っていけば、クライアントモジュールが出来上がる。

実際にRubyクライアント書いてみた

github.com

プロトコルJSONを読んでコード生成する部分が地味に大変だったけど、そのぶん実装部分はめっちゃ楽。なにこれ。ってかんじ。

require 'playwright'

Playwright.create(playwright_cli_executable_path: '/path/to/playwright-cli') do |playwright|
  playwright.chromium.launch(headless: false) do |browser|
    page = browser.new_page
    page.goto('https://github.com/YusukeIwaki')
    page.screenshot(path: './YusukeIwaki.png')
  end
end

このくらいの簡単なスクリプトを動かすだけなら、Browser, BrowserType, Page, Frame などの主要なクラスにいくつかのメソッドを実装するだけで、動くようになる。

puppeteer-rubyのときに散々苦しんだ、CDPSessionまわりの並行処理の順序変わっちゃう問題などは一切なく、(そのへんはサーバーモジュールがやってくれるので!)本当に素直にクライアント書くだけだ。

puppeteer-ruby のほうはしばらく開発をゆるめにして、Playwrightのほうを本腰入れて作っていこうかなと思う。