ブラウザオンラインゲームを Ruby on Rails で作る

この記事は、CAMPHOR- Advent Calendar 2021の21日目の記事です。

Ruby on Railshotwire-rails という gem を導入すると、ブラウザ・サーバー間の双方向通信がかんたんに実装できます。

それを利用して、ある種のブラウザオンラインゲームを簡単に実装にできるという話と、その一例として、オンラインで対戦できるリバーシを作った話をします。

続きを読む

ISUCON11を振り返る

@ebiebievidence@uni745e の2人と一緒に「ここにチーム名を入れる」というチームを組んで、ISUCON11に出場しました。

結果は予選敗退、最高スコアは45180、最終スコアは40952、公開Leaderboard での順位は 105位でした*1

この記事では、準備したこと、当日やったこと、振り返りについて述べます。

*1:参考値内での順位は100位でした

続きを読む

「はじめて学ぶソフトウェアのテスト技法」を読んだ

よく考えてみたら「テスト」について勉強したことなかったなと思って、本を読んでみた。

本の解説

「はじめて学ぶソフトウェアのテスト技法」は、バグを効率的に発見するためにテストをどう行えばよいかという問に答える本だ。 テストコードの保守性を上げるにはどうしたらよいか、という問には答えてくれないので、その問題意識を持つ人は別の本をあたったほうが良いと思う(良い本があったら教えてください)。

本書は、以下のブラックボックステストの技法を解説している。

我々がテストを書くときに無意識に実践している項目もある。例えば「同値クラステスト」は、ほとんどのプログラマーは無意識にやっているはず。 ただ、他のテスト技法に関しては、実践したことのないものばかりだった。

これまでは、テストでバグを発見できる気がしなくてすごく気持ち悪かったんだけど、この本を読んだことである程度自信を持ってやれそうな気がしてきた。

あと、すごく重要なことなんですが、この本はかなり読みやすいです。 昔似たような本を読んだときはとにかく退屈で、10ページぐらい読んでやめてしまったんだけど、この本は一人でもサクサク読みすすめられました。

面白かったところ

同値クラス・境界値

個人的には同値クラスと境界値が大事な発想だと思った。

テストっていうのは、原始的にはありうる全ての入力パターンを試して正しく動くことを担保しないといけないと思うんだけど、現実的にはそれは無理だ。 入力がenumとかで有限のパターンしか取らないなら可能かもしれないけど、整数とか実数が入力パラメータに入ってくるとオシマイだ。 そこで導入される考え方が「同値クラステスト」と「境界値テスト」だ。

振る舞いが「非線形」に変化する領域ごとに入力をグルーピングして、そのグループの「端の値」と「代表的な値」をテストしましょうね、というのが同値クラス・境界値テストだと自分は理解した。 同値クラス・境界値の発想を理解することで、整数とか実数とかの、無数の値を取る入力に対しても網羅的にテストが書けることになる。 このことによって、逆に「網羅的にテストを書こう」という気持ちになれることを発見した。

これまでテストを書くときは、以下のパターンでテストを書くことが多かった。

  • 網羅性を気にせず、ド正常系を1つと、思いつく限りの異常系をテストする
  • 正常系に属する範囲から入力をランダムに選んでテストする

これは、どうせ網羅的にはテストを書けないのだから網羅性を担保するのを諦める、という発想が根底にある。

網羅的にテストを行う手法を理解したことで、網羅的にテストを書こうという気持ちになれる。

また、同値クラスと境界値を前提として様々なテスト技法が存在している。 例えば、デシジョンテーブル同値クラスと境界値を前提として入力の網羅性を担保したいときに使う技法だし、ドメイン分析テストは同値クラス・境界値の拡張だ。 そういう意味でも、同値クラスと境界値を理解することは重要な雰囲気を感じる。

ペア構成テスト

ペア構成テストも面白い。 これは、単体では問題なく動くコンポーネント同士を組み合わせたときにバグるのを発見する手法だ。 組み合わせるコンポーネントの種類に対して、コンポーネントの組み合わせの数は指数的に増えていく。 そのため、全てのコンポーネントの組み合わせパターンをテストするのは実質的に不可能だ。 この問題を解決するために導入される手法がペア構成テストだ。

経験則として「組み合わせでバグるのは、2つのコンポーネントを組み合わせたときにバグるのがほとんどで、3つ以上のコンポーネントを組み合わせたときだけバグるというのはほとんどない」ということが知られているらしい。 つまり、コンポーネントの全組み合わせを調べる必要はなく、全てのコンポーネントがペアを組んだことがあるような組み合わせに絞ってテストをすれば、それだけでかなりの確率でバグを発見できる、ということだ。

「全てのコンポーネントがペアを組んだことがあるような組み合わせ」を生成するソフトウェアが世の中には存在するので、それを使ってテストする組み合わせをリストアップすればいい。 これによって、全組み合わせをテストするのに比べて格段にテスト対象を減らすことができる。

ただ、自動テストをするという観点からは、多くの場合は全組み合わせをテストすることは可能だと思う。特に正常系をざっとみるだけであれば、テストケースを自動生成してエラーがthrowしないことをチェックするのは簡単なはずだ。 一方で、各組み合わせに対して詳しい挙動をチェックしようと思ったら、ペア構成テストは自動テストにおいても有効な手法だと思う。

状態遷移テスト

これまで状態遷移をテストしたことなかったけど、いざやることになったら多分どこから手を付けたらいいかわからんとなっていたと思う。 この本には状態遷移をテストする方法がいくつか紹介されていて、それぞれどのぐらい偉いテストなのかということが書いてあってよかった。 重要な状態遷移なら気合を入れて偉いテストを書き、どうでもいいやつならざっくりテストを書く、ということができると思う。

まとめ

「はじめて学ぶソフトウェアのテスト技法」は面白い本だった。 雑にテストを書いてるソフトウェア開発者の人は一度読んでみると面白いと思う。

それはそれとして「いいテストコードの書き方」「テストが書きやすい設計をする方法」とかの知見はこの本からは得られないので、オススメの本がある人は教えてください(クリーンアーキテクチャは読みました)。

これは読書ノートです: 初めて学ぶソフトウェアのテスト技法 - genya0407のメモ

NOTE:

本書はブラックボックステストの手法だけでなく、ホワイトボックステストの手法も取り上げています。 しかし、ホワイトボックステストの扱いは小さいし、「ホワイトボックステストに拘るな」みたいな記述があったし、本体コードにゴリゴリに依存したテストコードの保守性はものすごく低いことが予測され、実際に業務で書くことはあまり無いだろうと思ったので、読み飛ばした。

とはいえ、「この分岐ってテストされてないけど大丈夫かな」のような発想でテストケースに気づけることもあると思うので、なんとなく意識するぐらいのことはしてもいいのかなと思った。

Rubyで設定を書けるLinux用キーマッパー 「rumap」をRustで作った

この記事は、 CAMPHOR- アドベントカレンダー 2020の8日目の記事です。

Rubyで設定を書けるLinux用のキーマッパーをRustで実装した話をします。

Rumap

Rumap は Ruby DSLで設定を書けるLinux用のキーマッパーです(正確にはX Window System用)。

github.com

キーマッパーとは何かというと、karabinarみたいなやつです。 つまり、キーボードの入力をなにか別の入力に変換するアプリケーションです。

例えば、以下のような設定ファイルを書いたとします。

remap 'Control-BackSpace', to: 'Delete'

これを rumap に食わせて起動すると、 Control と BackSpace を同時押しすると、代わりにDeleteが入力されるようになります。

また、キー入力を変換するだけでなく、キー入力をトリガーにしてコマンドを実行することもできます。 例えば以下のように書くことで、Alt と Shift と 4を同時押しすると gnome-screenshot -a -d 0 が実行されます。

remap 'Alt-Shift-4', to: execute('gnome-screenshot -a -d 0')

詳しい使い方やインストール方法は、READMEを参照してください。

設定ファイルがRubyスクリプトである

rumapの設定ファイルはRubyスクリプトなので、いろいろと便利なことができます。

例えばこういうことができます。

%w[r z x c v w t f Return].each do |key|
  remap "Alt-#{key}", to: "C-#{key}"
end

この設定ファイルを使うと、Alt と r や z や x...を同時押ししたとき、 Ctrlとの同時押し に変換されます。

このようにプログラミング言語を設定用の言語とすることで、複雑な設定も短く書き下すことができます。

なぜ作ったのか

Linuxで動いて、かつRubyで設定ファイルを書くことができるキーマッパーがなかったので作りました...なら理由としてカッコいいですが、実はそのようなキーマッパーは存在します。

k0kubun.hatenablog.com

DSLシンタックスもほぼ同じです(というかrumapのDSLはxremapのパクリです)。一時期は僕もxremapを使っていました。

ではなぜ作ったのかという話なんですが、xremap には with_modifier オプションが当時存在しなかったからです(今はあるかも)。

with_modifier オプション

with_modifier オプションの説明をするために、with_modifier オプションがない世界線の話をします。

Ctrl+hjkl で矢印キーを入力したい気持ちになることが人類にはよくあると思います*1。 これをxremapのDSLで表現するとどうなるでしょうか

remap 'Control-h', to: 'Left'
remap 'Control-j', to: 'Down'
remap 'Control-k', to: 'Up'
remap 'Control-l', to: 'Right'

こうなります。

これでめでたくCtrl+hjklで矢印キーが入力できるようになったわけですが、ところでテキストを編集するときにマウスを使わずに範囲選択をしたくなることは、人類にはよくあります。 Ctrl+hjkl のことを考えなければ、 Shiftを押しながら矢印キーを押してカーソルを移動する ことで、上記を実現できます。

「Shiftを押しながら」 なんだか怪しい雰囲気がしてきました。

実際、当時のxremapで、 Ctrl+hjkl の設定を入れた状態でShiftを押しながらCtrl+jを入力すると何が起こったかというと、何も起こりませんでした。 上の設定は Ctrl+j の設定であって、 Ctrl+Shift+j の設定ではないからですね。

xremapの動作をもう少し詳しく説明すると、

  • xremapは起動時に、Xのサーバーに対してwatchするキーの組み合わせを設定する
  • Xは、設定された組み合わせのキーが入力されたとき、その旨をxremapに通知する
  • xremapはキー入力の通知を受けると、キーを変換してXに入力する

のように動作するのですが、上の設定だと Ctrl+j の組み合わせしかwatchしないので、Ctrl+Shift+jが入力されてもxremapに通知が来ません。

これを解決するために、rumapで導入したのが with_modifier オプションです。

remap 'Control-h', to: 'Left', with_modifier: 'Shift'
...

このように with_modifier: 'Shift' を指定すると、rumapは立ち上げ時に、Ctrl+h に加えて Ctrl+Shift+hwatchします。そして、 Ctrl+h に変換し、Shiftも押されていた場合は Shift+← に変換します。つまり、 Ctrl+h (+Shift)← (+Shift) に変換します。

これによって、 Ctrl+Shift+hjkl を同時押しすることで、テキストの範囲選択が可能になります。

なんで本家にcontributeせえへんねん

当初は、xremapにpatchを当てることによって上記の問題を解決しようとしており。PRも出すには出しました。

Pass unmatched modifiers by genya0407 · Pull Request #36 · k0kubun/xremap · GitHub

この実装は、上で説明した with_modifier オプションとは違って、 全ての modifier の組み合わせをwatchします。 そして、通知されてきた入力のうち、設定されている部分について変換処理をします。

例えば、 remap 'Control-h', to: 'Left' と設定していたら、hを含む全ての入力(hCtrl+hCtrl+Shift+hCtrl+Alt+h、、、)をwatchし、Ctrl+Alt+j が入力されてきたら、 Alt+↓に変換する、といった感じです。

これは設定が短くて済むという利点はあるのですが、PRを出した直後にバグに気づいたのでPRを取り下げました。

もう記憶があんまりないのですが、たしか Alt+jCtrl+j みたいな設定をしていると、変換後の Ctrl+jj を含んでいるため通知が飛んできて、xremapが無限ループして死ぬ、というようなバグだったと思います。

この問題を解決するには、上で説明したような with_modifier オプションをDSLに導入すればよいのですが*2

  • DSLの文法についての交渉をするのが面倒だった*3
  • 既存のコードをいい感じに修正するのが大変そうだった(たしか)
  • xremapをコードリーディングして実装を概ね把握したことで、勉強も兼ねて自分で一から実装したくなった

などの理由から自分で実装することにしました。

その他、考えたことなど

作ってるときに考えたことなどをつらつらと書いていきます

x11ライブラリ

XのAPIを叩くために、x11というライブラリを使いました。これはXlibの非常に薄いラッパーで、Cの関数を呼ぶのでunsafeな操作ばかりです*4。ただ、薄いラッパーであることによって、Xlibのドキュメントがそのまま使えたのは助かりました。

一番困ったのはXlibのドキュメント(というかチュートリアル)がないことで、仕方がないので xremap の実装を何度も読み返しました。

Linux以外のOSに(理論上)拡張できるようにした

Linux以外のOSに拡張できるように、Linux(X)に依存する部分と、そうでない "コアロジック" を分離するようにしました。

これによって、(理論上は)例えばWindowsmacOSでも使えるように、rumapを拡張できるはずです*5

ただ、コアロジックと環境依存コンポーネントをうまく分離できていないのか、あるいは分離の仕方が悪いからかもしれませんが、コアコンポーネントのコードにジェネリクスが大量に発生してしまい、非常に読みづらく、またジェネリクス関連のコピペが多くなっています。

他環境にも拡張しないとただ単にコードの複雑性を増しただけになってしまうので拡張していきたい。しかし、macにはkarabinarがあるし、Windowsを使うことは基本的にないし、なかなかモチベーションが生まれないというのが正直なところです。

Cargoのworkspaceが便利

上に説明したコンポーネントの分離を実現するために、Cargoのworkspace機能を使ったのですが結構便利でした。

レポジトリを見てもらうと、 linuxmapper という2つのディレクトリが存在し、それぞれが1つのcrateになっています*6*7。 そして、Cargo.tomlに以下のように記述することで、1つのworkspaceとしてまとめています。

[workspace]
members = [
  "mapper",
  "linux",
]

この設定により、それぞれのcrateを、依存関係をもたせながら独立にビルドすることができます。

例えば、 linux/Cargo.toml に以下のように記述することで、linux crateからmapper crateを利用することができます。

[dependencies]
mapper = { path = "../mapper" }

また、以下のようにすることで、linux crate (と、それが依存するmapper crate)のみをビルドすることができます。

$ cd linux
$ cargo build

今回の例では全てのcrateがビルドされてしまうのでありがたみがないですが、例えば macos crate を追加したとき、Linux上では macos crate のビルドが通らないとしても、 linux crate のみをビルドすることが可能です。

共通機能を持ちつつもそれを利用したアプリケーションが複数あるような場合に、とても便利そうだなと思いました。

外部のRubyコマンドに依存するのをやめたい

xremap の「Rubyで設定が書ける」という特徴は革命的に便利なので、rumapでもこの特徴は引き継ぎたいと思っていました。xremapはmrubyで実装しているからrubyのコードを評価するのは簡単ですが、Rustでそれをどう実現すればよいでしょうか。

なんとrumapは、 外部のRubyコマンドを実行する ことで、設定ファイルを評価しています。具体的には、引数に指定されたファイルをinstance_evalして結果をJSON文字列にするRubyスクリプトを書いて、rumap起動時にそのスクリプトを起動して設定ファイルをJSONに変換し、JSONを元にrumapを初期化するようにしています*8

外部のRubyコマンドに依存するのはダサいし、外部要因で動いたり動かなかったりするのはつらいので、どうにかしたいと思っています。 この記事を書いてるときに見つけた以下のcrateを使えば、いい感じにできるはず。

github.com

まとめ

Rubyで設定を書けるLinux用のキーマッパーを作りました。ほとんど xremap のパクりですが、微妙に機能が増えています。

いろいろと実装上の反省点はあるのですが、自分が便利に使えているのでひとまずはいいかなと思っています。

*1:矢印キーまで指を伸ばすのはだるいので

*2:厳密には、Ctrl+h → Ctrl+h みたいな設定をしてると無限ループは回避できないけど、そんな設定をする人類はいないと仮定する

*3:よく考えると、後方互換性を保ったままDSLを拡張できるので割とすんなり行けたんじゃないかって気はする

*4:余談ですが、unsafeな操作は上で説明したコアロジックには存在しません。なぜなら、unsafeな操作をするのはX依存のモジュールだけだからです。

*5:実際には、X固有だと思ってたロジックがコアロジックだったとか、他の環境だとコアロジックが使い物にならん、みたいなのはあるとは思いますが、そのあたりはやってみないとわからない

*6:Cargo.tomlもそれぞれ持っている

*7:linuxがX依存部分、mapperがコアロジック

*8:JSONを作れるならなんでもいいので、例えばPythonDSLを作って組み込むことも可能なはずです。

標準入力からヒストグラムを描画するCLIツールを作った

標準入力をいい感じにヒストグラムにするCLIツールを作りました。

GitHub - genya0407/hist

インストール

Releases · genya0407/hist · GitHub からお好きなバイナリをダウンロードして、適当なパスに展開してください

使い方

なんかこういう感じのテキストファイルがあるとする。

$ cat example.txt
4.486107060301375
4.400612185880518
3.1836054290123
1.8814038706949097
3.367418962291763
2.5550752855238943
2.7646969681590603
4.099374705165457
4.765991107086257
3.1929965581891215
...(略)

これをhistコマンドの標準入力に食わせると、以下のようなAAが出力される。

cat example.txt | hist
466.1|
468.3|
470.5|
472.7|
474.9|**
477.1|**
479.3|****
481.4|********
483.6|*************
485.8|************************
488.0|******************************
490.2|******************************************
492.4|*****************************************************
494.6|*******************************************************************
496.8|**********************************************************************
499.0|********************************************************************************
501.1|***************************************************************************
503.3|*************************************************************************
505.5|*****************************************************************
507.7|*******************************************************
509.9|**************************************
512.1|***********************************
514.3|*******************
516.5|***************
518.6|*********
520.8|*****
523.0|**
525.2|*
527.4|
529.6|
531.8|
     +--------------------------------------------------------------------------------+ 995 times
     +----------------------------------------+ 497 times

縦軸が「値」で、横軸が「頻度」です。上の例だと、500ぐらいの値が出る頻度が一番高くて、その頻度は1000回ぐらいであることがわかります。

ちなみに、横軸の高さを調整するオプションや、値をグルーピングする単位(いわゆる bin)を調整するオプションもあります。

contributionは大歓迎です。あと、入力の行数が少ないときにバグっぽい挙動をするので注意してください。

モチベーション

アクセスログとかDBの中身をヒストグラムにしたいことは稀によくある。例えば「ユーザーあたりのブログポスト数ってどういう分布になってるのかな?」みたいなのを知りたいなど。

継続的に見ていきたいメトリクスならGrafanaとかのキチンとしたツールで見たほうが良いと思うんですが、今この瞬間サッと見れるだけでいいんだけど、というニーズも少なからずあります。そういうときにいちいちjupyter notebookを立ち上げて、seabornの書き方を調べながら、フォント環境が壊れててイライラしたり、gnuplotでいい感じにヒストグラムを書こうとして四苦八苦するのは不毛です*1

見たい対象のデータがどこにあるのかによっても違ってきますけど*2アクセスログをテキストファイル形式で扱う場合や、MySQLにクエリを打ってデータ取ってくるときは、シェルからデータが生まれてくるわけで、シェルのなかでグラフ描画までできたら楽でいいですよね。

というわけで、「シェルから生まれてきた数字列をそれっぽいヒストグラムAAに起こす」という処理を例によってRubyワンライナーで毎度毎度書いてたんですが、ちゃんとしたヒストグラムを書くのは意外にめんどくさいのでCLIツールにしちゃおと思ったのが、今回histコマンドを作ろうと思ったきっかけです。

メイキング

Rustの良いところ

CLIツールがいい感じに作れてすごかった(小並感)。

この argopt ってcrateがめっちゃ便利。

tanakh.hatenablog.com

詳しくは上のリンクを読んでもらったら良いんですけど、例えば以下のように書いてコンパイルして実行すると、

use argopt::cmd;

#[cmd]
fn main(
    #[opt(short = "b", long = "bin")] bin: Option<f64>,
    #[opt(short = "l", long = "bar-length", default_value = "80")] bar_length: i64,
) {
  // 略
}

以下のような出力が得られます。

$ hist --help
hist 0.1.0

USAGE:
    hist [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -l, --bar-length <bar-length>     [default: 80]
    -b, --bin <bin>      

もちろんmain関数の引数にはいい感じに引数が渡ってきます。これはちょっと感動的に便利ですよね。 今回RustでCLI作ろうと思ったきっかけの1/4ぐらいは、argoptの紹介記事を読んだことです。

余談ですが、最新のバージョン(v0.1.1)のargoptは、dependencyとしてargoptを指定するだけじゃ動かなくて、structoptも指定してあげないとダメっぽかったので、issueを立てて報告しました(英語が拙い)。

github.com

Github Actionsがべんり

これもすごかった。

motemen.hatenablog.com

この記事を真似するだけで、

  • タグをpushすると
  • macOS/Linux/Windows用バイナリのビルドが走り
  • 各種プラットフォーム向けのバイナリ一覧を含んだリリースが作成される

という魔法みたいな状態を構築することができます。当然ビルドされたバイナリは展開してPATHに置くだけで実行できる。シングルバイナリ最高です。今まで僕がRubyCLI作って配布方法をウンウン悩んでたのは何だったんだろう...

まとめ

標準入力からヒストグラムを描画するCLIツールをRustで作りました。RustはCLIツールを作成・配布する上でとても楽な選択肢であり、オススメです。

*1:もちろん、使い慣れてる人ならそんなに大変じゃないとは思いますけど、僕はあんまり使い慣れてないので...

*2:例えば、elasticsearchにアクセスログが入ってるケースとかは、histコマンドで取り扱うのには不適切かも。素直にkibanaを導入したほうが良さそう。