sauceLabsをつかってブラウザテストを自動化する

SauceLabs2.jpg

Sauce Labsというサービスがあって、WindowsやMac、iOSやAndroid等の各種OS上の各種ブラウザを仮想マシン上に立ち上げて、ブラウザ上でブラウザを操作することができるサービスです。
当初これがあればWindowsマシンを持ち歩かなくてもIE6のテストが出来るね!くらいの認識だったのですけど、説明を見ていると、どうやらSelenium WebDriver経由でユニットテストを実施することが本来の用途らしいので、コードをコミットしたら各種ブラウザでテストを実施してダメだったら教えてくれる環境というのを構築してみました。
selenium webdriverが登場した頃から、理屈の上ではできると言われていたテストですけど、あれこれの環境を個人で維持するのは現実的でないので、こういうのをサービスとして提供してくれるのは嬉しいですよね。

たとえばこういう、Google.comに行って検索した結果のページを表示するテストを実施するコードを用意して…

capsのところはSauce Labs: Supported Device, OS, and Browser Platformsからコピペすることが出来て、必要な設定に書き換えればMacのChromeでもWindowsXPのIE6でも、iOS7でもAndroidでも、任意のブラウザで同じテストを実施することが出来ます。

ということで、環境変数BROWSERでブラウザの種類を指定してunittestができる仕組みを作ってみました。

#Special thanks to Ruby-ML

通常の Test::Unit::TestCaseの代わりに SaucelabsTestCasePCを継承してテストを作れば、環境変数BROWSERで指定したブラウザでテストを実施することができるようになります。

$ env BROWSER=mac_chrome ruby pc/search.rb
Loaded suite pc/search
Started
.
Finished in 23.795709 seconds.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
Test run options: --seed 21266
$ env BROWSER=win_ie6 ruby pc/search.rb
:

テスト結果はunittestとして成功失敗を判定する以外にも、Saucelabs上ではこのようにキャプチャ画像や動画として保存されているものを見ることも出来ます。

SauceLabs7.jpg
SauceLabs6.jpg

ここまでできれば、あとはいつもどおりにunittestを書いてjenkinsさんにお願いしておけば、コードをコミットするだけで各種ブラウザの動作確認をしてくれるようになりますね。すばらしい。

ちなみにSauce Labsは月額課金のサービスで、月一時間位までは無料で使うことが出来ます。
1テスト30sくらいで実行できるので、全部のテストを一度通してみるくらいなら無料でも行けるけれど、本当にコミットするたびに全テスト走らせるのであれば、$12のManual会員か、$49のAutomated会員くらいは必要になるかもしれないですね。
今これを書きながら気づいたのですけど、githubに公開してあるコードは無料でテストできるらしいです。Sauce Labs: OSS

latlngの宗教問題

1406390_59235278.jpg

geocoderの話が予想以上にうけてびっくりしたので、ジオコーディングのくだらない話を一つ。

ジオコーディングに関するコードを書いているとたいていハマる問題が一つあって、それは緯度経度をどういう変数で表すか。
geo – Longitude の略し方。lng 派と lon 派の終わらない争い – Qiita
によると、だいたい世界には6つの派閥が存在するらしいです。

  • Lng派: lat,lng
  • Lon派: lat,lon
  • Long派: lat,long
  • Longitude(略さない)派: latitude,longitude
  • 緯度経度ペアに名前付けちゃう派: “34.12345,135.98765”
  • x,y派: latlngじゃなくてxとyで済ませちゃう人たち

個人的には、上記の他に、

[34.12345,135.98765]

とArrayで表す派閥が存在すると思っています。
例のRubyGeocoderは、もともと文字列を受け取ることになっていたのですが、最近Arrayも受け取るように修正が入ったようです。

ついでなので、Ruby界から、

{lat:34.12345,lng:135.98765}

と表す新Ruby派と、

{"lat"=>34.12345,"lng"=>135.98765}

と表す旧Ruby派なんかも参戦していただくと、問題が混沌としてきて、より宗教戦争らしくなってきますね。

609407_23691178.jpg

戦争を煽るばかりでは申し訳ないので、みんな仲良くできるコードを用意してみました。

緯度経度を表す多様なフォーマットを全部受け入れて任意のフォーマットで出力する試み
https://gist.github.com/mogya/6978002

これをつかえば、上記で上げたようなパターンをすべて扱うことが出来ます。

irb> GeoUtils::coordinates([34.12345, 135.98765])
=> [34.12345, 135.98765]
irb> GeoUtils::coordinates(["34.12345", "135.98765"])
=> [34.12345, 135.98765]
(以下、戻り値は略)
irb> GeoUtils::coordinates("34.12345", "135.98765")
irb> GeoUtils::coordinates(34.12345, 135.98765)
irb> GeoUtils::coordinates( {lat:34.12345, lng:135.98765} )
irb> GeoUtils::coordinates( {lat:34.12345, lon:135.98765} )
irb> GeoUtils::coordinates( {'lat'=>34.12345, 'long'=>135.98765} )
irb> GeoUtils::coordinates( {'x'=>34.12345, 'y'=>135.98765} )

出力側はgeocoderにあわせてarrayにしてあるけど、これも指定可能

irb> GeoUtils::coordinates([34.12345, 135.98765],:lat_lng)
=> {:lat=>34.12345, :lng=>135.98765}
irb> GeoUtils::coordinates([34.12345, 135.98765],:lat_long)
=> {:lat=>34.12345, :long=>135.98765}
irb> GeoUtils::coordinates([34.12345, 135.98765],:latitude_longitude)
=> {:latitude=>34.12345, :longitude=>135.98765}

こんなこともできます

irb> GeoUtils::coordinates(
[34.12345, 135.98765],
[:lat_lng,:lat_long,:lat_lon,:latitude_longitude,:x_y])
=> { :lat=>34.12345, :lng=>135.98765,
:long=>135.98765, :lon=>135.98765,
:latitude=>34.12345, :longitude=>135.98765}

使わなくて済んだはずのCPU時間を横着のために使うのはどうかという気もするのですが、気にならない方は自由に利用してください。

Ruby geocoderがすごい

住所を緯度経度に直したり、緯度経度から住所を求めたりする操作をgeocodingと言って、Google Maps APIを使うとまあたいていのことはできる。
ロケタッチAPIとか、Yahoo!ジオコーダAPIという手もある。

それはともかく、そのへんをパチパチ叩くコードを書いていて、「こんなのもうとっくに誰かが書いてんじゃないかなー」と思ってぐぐってみたらなんかすごいのが出てきた。


geocoder.png

Ruby Geocoder

住所と緯度経度の相互変換はもちろん、距離や範囲の扱い、Google以外のAPIへの対応、キャッシュ処理など、「実装しようかなー。でもめんどくさいよね」とか思って先送りしていたような機能がほとんど全部実装されている。

住所の取得

require 'geocoder'
# 日本語ロケールに設定
Geocoder.configure( :language  => :ja,   :units => :km )
Geocoder.address("東京タワー")
# => "日本, 〒105-0011 東京都港区芝公園4丁目4−2−8 東京タワー"

もちろん、住所しか取れないなんて馬鹿なことはなくて、GoogleMapsAPIの結果をそのままもらうこともできます

Geocoder.search("東京タワー")
# => [#
[{"long_name"=>"東京タワー",
"short_name"=>"東京タワー",
"types"=>["point_of_interest", "establishment"]},
:中略
{"long_name"=>"芝公園",
"short_name"=>"芝公園",
"types"=>["sublocality_level_1", "sublocality", "political"]},
{"long_name"=>"港区",
"short_name"=>"港区",
"types"=>["locality", "political"]},
{"long_name"=>"東京都",
"short_name"=>"東京都",
"types"=>["administrative_area_level_1", "political"]},
{"long_name"=>"日本",
"short_name"=>"JP",
"types"=>["country", "political"]},
{"long_name"=>"105-0011",
"short_name"=>"105-0011",
"types"=>["postal_code"]}],
"formatted_address"=>"日本, 〒105-0011 東京都港区芝公園4丁目4−2−8 東京タワー",
"geometry"=>
{"location"=>{"lat"=>35.6585805, "lng"=>139.7454329},
"location_type"=>"APPROXIMATE",
"viewport"=>
{"northeast"=>{"lat"=>35.6676459, "lng"=>139.7614403},
"southwest"=>{"lat"=>35.6495141, "lng"=>139.7294255}}},
"types"=>["point_of_interest", "establishment"]}>]

緯度経度から住所へ(リバースジオコーディング)

Geocoder.address("39.70150773,141.13672256")
# => "日本, 〒020-0034 岩手県盛岡市盛岡駅前通 県道2号線"
Geocoder.search("39.70150773,141.13672256")
[#"日本, 〒020-0034 岩手県盛岡市盛岡駅前通 県道2号線",
:(中略)
"types"=>["route"]}>,
#"日本, 盛岡駅前(バス)(岩手)",
:(中略)
"types"=>["bus_station", "transit_station", "establishment"]}>,
#"〒020-0034, 日本",
:(中略)
"types"=>["postal_code"]}>,
#"日本, 盛岡駅(岩手)",
:(中略)
"types"=>["train_station", "transit_station", "establishment"]}>,
:(以下略)

IPアドレスの場所を取得

Geocoder.search("210.194.198.60")
[#"210.194.198.60",
"country_code"=>"JP",
"country_name"=>"Japan",
"region_code"=>"19",
"region_name"=>"Kanagawa",
"city"=>"Yokohama",
"zipcode"=>"",
"latitude"=>35.4478,
"longitude"=>139.6425,
"metro_code"=>"",
"areacode"=>""}>]
Geocoder.address("106.187.50.98")
# => "Japan" # IPアドレスによってはこうなってしまう(笑)

距離や角度の演算

t1 = Geocoder.search("東京タワー")[0].geometry['location'].values.join(',')
t2 = Geocoder.search("東京スカイツリー")[0].geometry['location'].values.join(',')
Geocoder::Calculations.distance_between(t1,t2)
# => 8.20771162081327 # km
Geocoder::Calculations.bearing_between(t1,t2)
# => 51.834180520275936 # degree
Geocoder::Calculations.compass_point Geocoder::Calculations.bearing_between(t1,t2)
=> "NE"
# railsと組み合わせるとobj.distance_to みたいなこともできるらしい。

Bing APIを使ってみる

# APIキーが必要。http://www.bingmapsportal.com
Geocoder.configure( :lookup => :bing, :api_key=>'*****' )
Geocoder.search("東京タワー")
# => [#"Location:http://schemas.microsoft.com/search/local/ws/rest/v1", "bbox"=>[35.62964091461487, 139.69803606567407, 35.687581680599976, 139.79308331909155], "name"=>"Tokyo Tower, Japan", "point"=>{"type"=>"Point", "coordinates"=>[35.65861129760742, 139.7455596923828]}, "address"=>{"countryRegion"=>"Japan", "formattedAddress"=>"Tokyo Tower, Japan", "landmark"=>"Tokyo Tower"}, "confidence"=>"High", "entityType"=>"LandmarkBuilding", "geocodePoints"=>[{"type"=>"Point", "coordinates"=>[35.65861129760742, 139.7455596923828], "calculationMethod"=>"Rooftop", "usageTypes"=>["Display"]}], "matchCodes"=>["Good"]}, @cache_hit=nil>]
Geocoder.address("東京タワー")
# => "Tokyo Tower, Japan"

ほかにも、API呼び出しをキャッシュさせたり、Railsと組み合わせることで Hotel.near(“Vancouver, Canada”) みたいなこともできるようになるらしいです。

もっと早く探しておけばよかったなー。

railsマルチプロセスとログとlogrotate

先日パーティにお招きいただいたM社の方から、mongrel_clusterで複数プロセス立ち上げている時、logroateすると、ひとつのプロセスを残して、ログファイルを見失って正常に動作できなくなる、というご相談をいただきました。

美味しいお酒をいっぱいのませていただいたのでちょうど僕も使うアテがあるので、僕がよく使っているthinのケースを書いてみます。たぶんmongrelでもそんなに変わらないんじゃないかな〜。

マルチプロセスで起動する

・セッションをファイルじゃなくてDBに保存するようにする。やり方はググれば出てくるので省略。
・ログはproduction.logじゃなくてSTDERRに出力して、thinの方でファイルに書いてもらう。具体的には、config/environments/production.rbとかで

  config.logger = Logger.new(STDOUT)

という具合に。
・thinの設定

$ sudo ln -s /var/www/example/api/config/thin.conf /etc/thin/example.yaml
$ sudo emacs /var/www/example/api/config/thin.conf
---
chdir: /var/www/example/api
environment: production
servers: 3
address: 0.0.0.0
port: 3000
timeout: 30
log: log/thin_example.log
pid: tmp/pids/thin_example.pid
max_conns: 1024
max_persistent_conns: 100
require: []
wait: 30
daemonize: true

これでlocalhost:3000,3001,3002と3プロセスが立ち上がります。
ログは、

$ ls -la api/log/
-rw-r--r--  1 root    root     3081 Aug  6 20:42 thin_example.3000.log
-rw-r--r--  1 root    root     1292 Aug  6 20:42 thin_example.3001.log
-rw-r--r--  1 root    root    15632 Aug  6 20:42 thin_example.3002.log

という具合にプロセスごとに別々に出力されます。

Rails3.2からログの行が他プロセスのものと混ざるようになった件について – 昼メシ物語 などを見ていると、一つのファイルにかいた時ログが混じるという話が出ているのですけど、そもそも複数プロセスから一個のファイルに書き込む時点で怖くて仕方ないので、
最初から分けてあったほうがいっそスッキリするんじゃないかな〜。

ログが複数にわかれていても、

tail -f log/thin_example.*.log

とすれば一つの窓で監視することができます。

ロードバランサー(apache)の設定

 こんなかんじで

$ sudo ln -s /var/www/example/api/config/httpd.conf /etc/httpd/conf.d/example.conf
$ emacs /var/www/example/api/config/httpd.conf
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot "/var/www/example/html/"
ServerAdmin mogya+example@mogya.com
ErrorLog "/var/www/example/log/error_log"
TransferLog "/var/www/example/log/access_log"
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
CustomLog /var/www/example/log/custom_log combined
ProxyPass /api/v2 balancer://example
ProxyPassReverse /api/v2 balancer://example
</VirtualHost>
<Proxy balancer://example>
BalancerMember http://localhost:3000
BalancerMember http://localhost:3001
BalancerMember http://localhost:3002
</Proxy>

いまどきapacheなんて・・・という方はherokuでもAWSでもお好きなフロントエンドでどうぞ。

logrotate

sudo ln -s /var/www/example/api/config/logrotate.conf /etc/logrotate.d/example
emacs /var/www/example/api/config/logrotate.conf
/var/www/example/api/log/* {
daily
missingok
rotate 1000
notifempty
copytruncate
create 0666 root root
sharedscripts
postrotate
/etc/init.d/thin restart > /dev/null
/etc/init.d/httpd restart > /dev/null
endscript
dateext
}

thinだけrestartだと直後の挙動が怪しかったので、httpdもrestartするようにしてあげたほうがいいみたいです。

$ sudo logrotate -dv /etc/logrotate.d/example

で文法チェック。

$ sudo logrotate -f /etc/logrotate.d/example

で動作確認を行うことができます。

環境

$ rails -v
Rails 3.2.13
$ thin -v
thin 1.5.1 codename Straight Razor
$ httpd -v
Server version: Apache/2.2.24 (Unix)
Server built:   May 20 2013 21:12:45

svgで画像ファイルを量産する

この記事は、Titanium mobile “early” Advent Calendar 2012の提供でお送りしております。

Titanium Mobileのおかげで、iPhoneとAndroid両にらみでアプリ開発することが現実的になったのはいいのですけど、どちらも機種が増える一方なので、画像ファイルの準備が洒落にならなくなりつつあります。
最低限必須となるアイコンとスプラッシュスクリーンだけで50種類!

Titaniumで用意する画像のサイズ – Sawalog

とはいえ、実際にはほとんど同じ画像がサイズの違うキャンバスに貼り付けてあるだけなので、自動化することにしました。

tiqiita.png

最大で1024pxの画像を57pxまで圧縮することになるので、ビットマップやpng画像をそのまま縮小すると目も当てられない画像になってしまいます。そこで、デザイナさんにはillustratorのベクター画像で納品をお願いすることにしました。
illustratorの「Webおよびデバイス用に保存」機能を使うと、作成していただいた画像をSVGフォーマットで出力することが出来ます。
illustrator.png

SVGは、ベクターグラフィックスなので縮小してもつぶれない、たいていのブラウザでも開くことが出来るなど、期待される要素がたくさんある割にはちっとも日の目を見ないフォーマットですが、それはさておき、プログラマ御用達の画像処理ツールImageMagickやそのRubyバインディングであるRMagickで扱うことが出来ます。

というわけでRubyで書いたコードがこちら。icon.svgとsplash_screen.svgを元に、必要なサイズに拡大縮小、背景は指定した色で塗りつぶした画像を一気に生成します。

こういうのを用意しておけば、「iPhone6が発売されたので新しい解像度の画像が必要」というときでも、プログラマレベルで片付けることが出来るので便利ですよね。

ただこの手法にはちょっと難点があって。ドロップシャドウとか非表示レイヤーなんかはImageMagickがついてこれないのでぐちゃぐちゃになってしまうことがあります。パスで描かれた単純な画像しか適用できないのが辛いので、最近はPhotoShopのアクション機能で出力するほうがいいのかなーとか考えています。そのへんも手法が確立したらそのうちブログで書こうと思います。

[ruby]電源マップをメールで送信

モバイラーズオアシスAPIの使用サンプルをもう一つ。こちらは、モバイラーズオアシス 電源マップAPIを利用して、指定された位置の電源マップのURLをメールで送るサンプルです。

メールを送るためには、メールを送るためのSMTPサーバが必要になります。
とはいえgmail全盛の今の時代、メールサーバを立てるといわれてもピンとこない人も増えているので、そういう方はgmailのメールサーバを利用するのが良いかと思います。
「gmailのメールサーバを使う場合」のコメントを外してユーザー名とパスワードを設定すれば、自前のメールサーバの代わりにgmailのサーバを使うことが出来ます。

dengenmail2.png
dengenmail.png

[ruby]モバイラーズオアシスAPIの使用サンプル

Mashup Awards 8 (#MA8)に提供しているモバイラーズオアシスAPI。「Rubyでのサンプルが欲しい」との声をいただきました。
とっくに応募期間が終わって表彰式当日ではありますが、今後もAPIを利用していただく機会はあるだろうと思うので、一応サンプルを用意しました。

あまり上手い例を思いつかなかったので、ごくシンプルに、コマンドラインでもらった緯度経度周辺のお店をリストアップするコマンドです。

Ruby1.9を想定して書いてありますが、1.8でも、JSONライブラリを入れれば動くんじゃないかと思います。

moapi.png

実践スクレイピング/第42回 Ruby/Rails勉強会@関西

今日は第42回Ruby/Rails勉強会@関西。「実践スクレイピング」というテーマで、電源検索サイトモバイラーズオアシスのデータを集めるために行ったスクレイピングについてお話しさせていただきました。

会場で教えていただいたこと。

  • IRB.conf[:ECHO] = nil でirbの応答がでないように設定可能
  • irb –noecho でも可

okkezさん、cyrossさん、ありがとうございます!

[ruby]「嶽」の文字化け問題

Ruby1.8.6で「嶽」の字をtosjisでShift-JISに変換すると、なんだか意図しない文字(F7 D3 0A)に変換されてしまった。

require 'kconv'
str="嶽"
print str.tosjis

(utf-8で保存して実行してね。)

Rubyist Magazine – 標準添付ライブラリ紹介 【第 3 回】 Kconv/NKF/Iconvによると、

>先述の Kconv#to* では変換元の文字コードを推測しているため、推測が外れていた場合は変換結果が文字化けしてしまいます。このような危険性を避けるため、変換元の文字コードが分かっている場合は、なるべく文字コードを明示的に指定するようにしましょう。

とのこと。

print str.kconv(Kconv::SJIS, Kconv::UTF8)

でちゃんと変換できた。

“嶽 文字化け”でググっても出てこなかったので、とりあえず記事にしておこう。
ちなみに、「佐渡ケ嶽部屋」もダメだった。「嶽」の字が鬼門らしい。