読者です 読者をやめる 読者になる 読者になる

初めてのpull request

Githubを利用してpull requestを送ったら受け取ってもらえてとてもうれしかったです。
今後も続けていきたいのでなんでpull requestを送ったのか思いを残しておきます。

どんなpull requestを送ったのか。

em-websocket-clientで利用されているWebSocketのバージョンがhixie-76だったので最新版のRFC6455のものを利用するよう修正しました。

なぜ送ったのか

Goliathを利用したアプリケーションを書いているときのテストで利用されていたのだけどもどうも挙動がおかしいなと思ったので、内部のコードを追ってみたら気づきました。Github見たら全然更新されてないし送ってみようかなと思ってとりあえず修正してみました。
そしてどうせだめでもいいやという気持ちで適当な英語で送ってみました。

結果

返事は2週間くらいしてからきました。無事受け取ってもらえてありがとうと一言。こんな手軽にやりとりできて世界の開発者とつながれる体験はとても素晴らしいものでした!

f:id:POCHI_BLACK:20121223212934p:plain

yeoman+AngularJS+testacularでEnd to endテストする

テスト実行の組み合わせ

AngularJSを利用してます。
ドキュメントが豊富で覚えやすく、テストシナリオも実行できるというのがとても気に入っています。
また、開発ツールyeomanというツールも気にいっていて、便利に使っています。
今日はyeomanを利用してテストをしてみるとこまでのログを残しておこうと思います。

yeomanのインストール

以下のコマンドでインストールできます。
がいろいろ足りないといわれるのでがんばります。
主にbrew使えば入ります。
またNodeJSが0.6とかだと動かないので注意が必要です。
(0.8とかあれば大丈夫です)

$ curl -L get.yeoman.io | bash
ひな形作成

yeomanでAngularJSのひな形を作ってみます。

$ mkdir angulartest
$ cd angulartest
$ yeoman init angular

これでずらずらっと中身ができたらオッケーです。

Angular JSシナリオテストの用意

testacularでは単体テストEnd to endテストは分けないと実行できないようです。
今回はtest/e2eディレクトリを作成してその中でEnd to endテストを実行するようにします。

testacular.conf.jsの設定ファイルをコピーしてtestacular-e2e.conf.jsのようなファイルを作ります。
そしてfilesを以下のようにします。

files = [
  ANGULAR_SCENARIO,
  ANGULAR_SCENARIO_ADAPTER,
  'app/scripts/vendor/angular.js',
  'test/lib/angular-mocks.js',
  'app/scripts/*.js',
  'app/scripts/**/*.js',
  'test/e2e/*.js'
];

こうすることでtestacularはプラグインであるAngularJSのテストファイルを読み込み、DSLが利用できるようになります。AngularJSのテストランナーAPI"こちら"にあります。

実際のViewを検証するWebServerの設定

正しいかはわかりませんEnd to endのテストを行う場合、クライアントに表示するViewを返却するサーバを起動する必要があるようです。
yeomanで作ったひな形にはそのWebサーバがないため、それ用のスクリプトを用意する必要があります。
angular-phonecatのもの(scripts/web-server.js)をそっくりそのままもってくると利用できるのでそれをangulartestにも持ってきます。

以下のコマンドで起動して http://localhost:8080/ にファイル一覧画面のようなものが開くか確認します。

$ node scripts/web-server.js
End to endの設定ファイルに立ち上げたWebServerの設定をする

End to endのテストは自分でサーバを立ち上げてテストを行いますがアプリケーションを立ち上げるという感覚ではなくテスト用サーバをたてるイメージだと認識してます。
ここでテスト用サーバが実際のアプリケーションを呼び出せるように、先ほどの立ち上げたWebServerを見に行くような設定を行います。
testacular-e2e.conf.jsに以下のような設定を加えます。

proxies = {
	'/': 'http://localhost:8000'
};

これでAngularのシナリオAPIで利用するbrowser().navigateTo()などは与えられたURLを参照する際にこのプロキシを参照します。
ここで初めてテストしたいアプリケーションが見れます。(はずです)

テストを書く

ここまできてやっとテストがかけます。
yeomanのデフォルトはPhontomJSでテストするようになっていますがChromeに変更しておこうと思います。

testacular-e2e.conf.jsのbrowsersを以下のように変更します。

browsers = ['Chrome'];

テストの内容ですがyeomanをinitで生成した際のhtmlのh1タグに'Cheerio!'という文字が表示されるのでそれが正常に表示されることを確認してみます。
test/e2e/scenarios.js に以下のような内容を書いてみます。

'use strict';

describe('Initial Url', function() {

	it('should contain "Cherrio"', function(){
                // 相対パスでプロキシめがけて表示したいhtmlを指定
		browser().navigateTo('../../app/index.html');
		expect(element('').text()).toContain('Cheerio!');
	});

});

これを以下の手順で起動して、テストします。

//先ほどもらってきたプロキシサーバを起動
$ node scripts/web-server.js
// テストサーバを起動
$ testacular start testacular-e2e.conf.js
// テスト実行
$ testacular run testacular-e2e.conf.js 
Chrome 23.0: Executed 1 of 1 SUCCESS (0.335 secs / 0.21 secs)

これでテストが通りました。
まだまだJSおよびNodeに関する能力が低くて困りますががんばって勉強します。
今回作ったアプリはここにあります。

web-socket-jsのFlash接続について

web-socket-jsとは

WebSocketに対応していないブラウザに対してFlashを利用して擬似的に
WebSocket接続を行うもの。
socket.ioなどのライブラリはWebSocketに対応していないとFlashやLong Cometに置き換えるがweb-socket-jsは一貫してWebSocketを貫き通す。

ソースコードGithubにあがっているのでとても使いやすい。
対応ブラウザもGithub上のREADMEに書いてある。

Flash接続について

web-socket-jsを利用する場合十分に考慮しないといけないのがFlashのSocket接続だと思う。
ソケットポリシーファイルのやり取りが必要になりFlashの仕様として、以下の手順を踏むことになっている。(リンク先引用)

  1. まず843番ポート(ソケットマスターポリシーファイル)にアクセス。繋がらない or 3秒以内に返事がない場合は、次へ。
  2. Security.loadPolicyFile("xmlsocket://...");で指定されたポートがあれば、そこにアクセス。繋がらない or しばらく(20秒ぐらい?)返事がない場合は、次へ。URLがxmlsocket://...の場合のみ有効なので注意。Security.loadPolicyFile("http://...");で指定されたファイルはSocket、XMLSocketによる通信では無視される。
  3. Socketの接続先と同じポートにアクセス。適切なソケットポリシーファイルが受信できなければ、SecurityError
843ポートに接続する

一番最初にここを見に行くのでここでソケットマスターポリシーファイルを返せるなら一番これがいい。でも、大体社内では閉じられていたりするので厳しかったりもする。
ちなみに閉じた状態で試してみると3回SYN飛ばして諦めてた。

Security.loadPolicyFileで接続する

ここは設定次第で接続しにいくみたいだけどよくわからなかった。
web-socket-jsではWebSocket.loadFlashPolicyFileを事前に設定すると呼ばれないようだ。
(loadFlashPolicyFileの設定先はFlashが読みにいくそれとは違うみたい)

Socketの接続先と同じポートにアクセス

最後にここにたどり着く。
web-socket-jsの作者が開発されているwebsocket-rubyでは以下のように対応されている。以下引用。

    def create_web_socket(socket)
      ch = socket.getc()
      if ch == ?<
        # This is Flash socket policy file request, not an actual Web Socket connection.
        send_flash_socket_policy_file(socket)
        return nil
      else
        socket.ungetc(ch) if ch
        return WebSocket.new(socket, :server => self)
      end
    end

    # Handles Flash socket policy file request sent when web-socket-js is used:
    # http://github.com/gimite/web-socket-js/tree/master
    def send_flash_socket_policy_file(socket)
      socket.puts('<?xml version="1.0"?>')
      socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
        '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
      socket.puts('<cross-domain-policy>')
      for domain in @accepted_domains
        next if domain == "file://"
        socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
      end
      socket.puts('</cross-domain-policy>')
      socket.close()
    end

Socketから取得したデータに対して"<"で始まっていればポリシーマスターファイルを返している。
実際Flashからリクエストが来た時の中身はこんな感じ。

<policy-file-request/>\x00

URLのパスとか全く考慮されてないので柔軟には扱えないかなと思う。

まとめ

長くなってしまったけどweb-socket-js使うときはこの流れを考慮したほうがいいと思う。
em-websocketなどはsamplesに843で返す簡易サーバがあるし、デフォルトでも対応してる。他のライブラリでもどの手順でポリシーファイルを意識するかは大事だと思う。
ちなみにChromeはポリシーファイル返してすぐWebSocket接続リクエストが来たけどFirefoxだとタイムラグが1.5秒くらいあいてた。

ActiveSupportのHash拡張

active_support/core_ext/hash/ 以下をざっとみてみた。

slice.rb

sliceは引数の要素を含むHashを返しslice!はオブジェクトごと書き換える。
extract!は引数のkeyを削除し、戻り値は削除したHash。

irb(main):001:0> require "active_support/core_ext/hash"
=> true
irb(main):002:0> hash = {a:1,b:2,c:3,d:4}
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):003:0> hash.slice(:a, :c)
=> {:a=>1, :c=>3}
irb(main):004:0> hash
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):005:0> hash.slice!(:a, :c)
=> {:b=>2, :d=>4}
irb(main):006:0> hash
=> {:a=>1, :c=>3}
irb(main):007:0> hash = {a:1,b:2,c:3,d:4}
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):008:0> hash.extract!(:a, :c)
=> {:a=>1, :c=>3}
irb(main):009:0> hash
=> {:b=>2, :d=>4}
irb(main):010:0> 
diff.rb

Hash#diff(other)はotherと同じ要素と値のペアがあればそれを削除し、かつ
otherと同じkeyがあって値が違うものをmergeする。

irb(main):002:0> require "active_support/core_ext/hash/diff"
=> true
irb(main):003:0> hash = {a:1,b:2,c:3,d:4}
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):004:0> hash.diff(a:1)
=> {:b=>2, :c=>3, :d=>4}
irb(main):005:0> hash.diff(a:3)
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):006:0> hash.diff(a:3,c:3)
=> {:a=>1, :b=>2, :d=>4}
irb(main):007:0> 
except.rb

Hash#except(keys)とHash#except!(keys)を定義。exceptは自分をdupしてexcept!を呼ぶだけ。
Hashから引数のkeyを差し引いたものを返す。

irb(main):007:0> require "active_support/core_ext/hash/except"
=> true
irb(main):008:0> hash = {a:1,b:2,c:3,d:4}
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):009:0> hash.except(:a, :c)
=> {:b=>2, :d=>4}
irb(main):010:0> hash
=> {:a=>1, :b=>2, :c=>3, :d=>4}
irb(main):011:0> hash.except!(:a, :c)
=> {:b=>2, :d=>4}
irb(main):012:0> hash
=> {:b=>2, :d=>4}
irb(main):013:0> 
keys.rb

Hashのkeyに関するメソッドが入っている。
Hash#transform_keys #=> block渡してkeyを処理する。
Hash#stringify_keys #=> keyを文字列にする
Hash#symbolize_keys #=> keyをシンボルにする
Hash#assert_valid_keys #=> keyを検証する
Hash#deep_transform_keys #=> block渡してkeyを処理する。
Hash#deep_stringify_keys #=> keyを文字列にする
Hash#deep_symbolize_keys #=> keyをシンボルにする

続きは次回