やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

台湾のVR体験施設『VIVELAND』を見てきたメモ

武術関係の用事で台湾に行ったついでに、期間限定オープンしているHTCのVR体験施設『VIVELAND』を見てきました。

VIVELANDは、台北市における秋葉原 ポジションである『光華商場』ビルの隣、『三創生活園區』の3Fにありました。このビル、他にはHTCやMSIのショップが入っています。オーナーは鴻海だそうです。

三創生活園區の場所はここ。地下鉄駅からすぐ。

3Fの一角にVIVELAND

f:id:nowsprinting:20161113055443j:plain

通路側から見えるところに、制限時間内にいくつかのVIVEコンテンツを遊べるブース。200NTD(およそ600JPY)で15分間。物価*1を考えると強気な設定。

f:id:nowsprinting:20161113055409j:plain

写真では見づらいですが、アーチ状の天井に可動式の竿みたいなものが生えていて、そこからHMDのケーブルが出ています。 ベースステーションもアーチのところに設置。

コンテンツは、Steamで買えるもののほか、オリジナルのものもあるみたい。

※表の下3つは専用ブースのものなので、自由に遊べるものの対象外。

一番人気らしい*2、『Project CARS』。250NTD(およそ750JPY)/1ゲーム。これも通路側に設置。

二番人気、FPSの『FRONT DEFENSE』。200NTD(およそ600JPY)/1ゲーム。これも通路側。左手の壁から竿が延びていて、HMDのケーブルがつながっています。上にあるモニタは、プレイヤーのHMDに映っている映像が流れていました。

土嚢に隠れたり、地面から何か拾ったり、忙しそうなFPSでした*3

受け付けを通って奥に入ると、こんな感じのブースが4つ。オープンして間もない平日昼間ということで、まだここまで入ってくるお客さんは少ないみたい。

一番奥には、どこかで見たことがあるような、細い板を渡るっぽいブースが!

この高所恐怖SHOW『命懸一線』というコンテンツはすぐ体験できたので、やってみることに。150NTD(およそ450JPY)/1ゲーム。

受け付けでお金を払うと、これまたどこかで見たような、でもちょっと違うマスクを渡されます。へーこれを被ってからHMDを着けるのかー(棒

ブースに戻ると、板の手前に立たされ、足にマジックテープでVIVEコントローラーを取り付けてもらいます。手には何もなし。

HMDとスピーカーを装着してプレイ開始。なんとなく高所恐怖SH◯Wと比べてしまいますが、

  • スタートは、まず足元を見て、続いて正面の円を見ることで周囲(ビルの屋上)が描画されて開始。エレベーターで上昇するような演出はなし。
  • 猫を助ける(行って戻る)のではなく、背後から追い立てられるという趣向
  • 導線は弱く、お姉さんに「後ろを見て」と言われてはじめて趣旨を理解する始末。台湾語ではちゃんと導入があるのかも知れない(一部スマートフォンの翻訳を使いつつの英語で説明してくれてたので)
  • 板は不安定になっていて、ちゃんとガタガタ揺れる
  • 板の先には当初なにもないけれど、ヘリコプターが迎えに来るので乗り移る(半歩で届く至近距離に無風でホバリングするヘリにプレゼンスは無い)。

所感

お台場の『VR ZONE』でもHTC VIVEを使用していた関係で、結構オフィシャルにノウハウを吸い上げていたのではと予想していたのですが、少なくともコンテンツの作りはまだ色々足りていない感じ。

スタッフの練度もまだ低いかも知れませんが、みんなフレンドリーに接してくれて印象はよかった。

平日の昼間ということもあり、お客さんは通路沿いの『Project CARS』と『FRONT DEFENSE』だけ数名の列、ほかは2組だけとまばらでした。現地の新聞やテレビのニュースでも取り上げられていたそうで、これから先、もっとお客さん増えて台湾のVRも盛り上がるといいですね。

参考

www.vive.com

www.moguravr.com

HTC、台北の三創生活園区にHTC VIVEを体験できる「VIVELAND」を期間限定でオープン – PANORA

htc.hatenablog.com

*1:地下鉄初乗りが20NTD、缶ビールが40NTDくらい

*2:スタッフのお兄さん談

*3:これもプレイしたかったのですが、無理を言っての別行動だったので並んでまでプレイする時間は無かった

GAE/Goで動くLINE BOTのテストを書いてみた

Google App Engine Go(以下GAE/Go)上で動くLINE BOT調整さんリマインダBOT」のMessaging API対応やグループ対応をしつつ、テストを書いて得た知見のメモ。

環境

環境変数

API KEYなどをapp.yaml環境変数として定義している場合、テスト実行($ goapp test)では値を取得できません。

なので、Makefileにテスト用の値を定義し、常に$ make testで実行するようにしました。

export LINE_CHANNEL_SECRET=012345678901234567890123456789ab
export LINE_CHANNEL_ACCESS_TOKEN=u012345678901234567890123456789ab

test:
    goapp test -v

実際のMakefileでは、カバレジ取得、-runオプションの付与も行なっていますが割愛。

aetest package

プロダクトコード内でappengine.NewContext(http.Request)context.Contextを取得している場合、httptest.NewRequest()などで生成したhttp.Requestを渡すとエラー*1になります。

そのため、GAE/Goのテストではaetest packageを使用してcontext.Contexthttp.Requestを生成して使う必要があります。

Contextだけが必要な場合

データストアに関するテストを書く場合などContextだけが必要な場合は、以下のように取得できます。

c, done, err := aetest.NewContext()
if err != nil {
    t.Fatal(err)
}
defer done()

aetestを使用すると、GAE/Goのローカル開発サーバ( local development server)が起動します。

これには都度(テストケースごとに)起動に時間がかかるのと*2defer done()を忘れるとプロセスが起動したまま残ってしまう*3ので注意しましょう。

Instanceが必要な場合

http.Requestを使うテストの場合には、以下のようにGAEインスタンスを直接取得します。引数のaetest.Optionsは、データストアを使用しないならnilでも構いません(後述)。

opt := aetest.Options{StronglyConsistentDatastore: true}
instance, err := aetest.NewInstance(&opt)
if err != nil {
    t.Fatalf("Failed to create aetest instance: %v", err)
}
defer instance.Close()

こちらも、defer instance.Close()を忘れずに。

取得したインスタンスから、以下のようにhttp.Requestcontext.Contextを生成できます。

req = httptest.NewRequest("POST", "/line/callback", json)
c := appengine.NewContext(req)

なお、appengine.NewContext()は、例えばプロダクトコード内とテストコード側で二回記述されていても、インスタンスが同じなので同一のデータストアを扱うことが出来ます。プロダクトコードの引数にhttp.Requestがあれば、無理にcontext.Contextまで渡す必要はありません。*4

http.RequestのContent-Typeヘッダ

テストに使うhttp.Requestを自力で組み立てる場合、以下のようにContent-Typeヘッダを付与しないとBodyが渡りません。

Task Queueなど、url.ValuesをEncode()する場合

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

jsonの場合

req.Header.Set("Content-Type", "application/json")

WebhookのX-LINE-Signatureヘッダ

LINE Messaging API SDKにパースさせるWebhookのリクエストには、リクエストヘッダにSignatureを乗せる必要があります*5。 Signatureは、API Referenceの"Webhook Authentication"に書かれている検証手順を元に、以下の手順で生成できます。

  1. Channel Secretを秘密鍵として、HMAC-SHA256アルゴリズムによりRequest Bodyのダイジェスト値を得る
  2. ダイジェスト値をBASE64エンコードした文字列を、Request Headerに付与する

具体的には以下のコードで生成・付与できます。

channelSecret := os.Getenv("LINE_CHANNEL_SECRET")
hash := hmac.New(sha256.New, []byte(channelSecret))
hash.Write(byteBody)
encoded := base64.StdEncoding.EncodeToString(hash.Sum(nil))
req.Header.Add("X-LINE-Signature", encoded)

httpmock

httpmockは、テスト実行時に外部サーバのスタブとして動作します。Goではhttptest.NewServer()で簡単にスタブを立てることはできるのですが、今回のようにアクセス先URLがSDK内に隠蔽されているケースなど、アクセス先URLを書き換えずにモックできるので便利です。

本家(jarcoal/httpmock)はメンテナンスが止まっているので、forkされて継続開発されているこちらを使用しました。

github.com

httpmockの有効化

以下のコードで有効化できます。単にhttpmockを有効化すると、以降すべてのURLに対するリクエストをhttpmockが受け取り、エラーレスポンスを返すようになります。

ctx := appengine.NewContext(req)
client := urlfetch.Client(ctx)

httpmock.ActivateNonDefault(client)
defer httpmock.DeactivateAndReset()

ここで重要なのは、引数付きのActivateNonDefault()を使い、appengine.urlfetchインスタンスを渡している点です。

GAE/Goでは、外部へのhttpリクエストは(DefaultClientではなく)urlfetch.Client()で生成したクライアントから送る必要があります。そのため、linebotクライアントもurlfetchを使って初期化しています(過去記事「調整さんリマインダLINE BOTを作ってみた - やらなイカ?」参照)。

httpmockはhttp.Clientインスタンスに対して効力があるため、linebotクライアントに渡したものと同じインスタンスActivateNonDefault()に渡す必要があります。

スタブの定義

例えば、LINEのReply Message APIが単に正常終了するスタブであれば、以下のように記述します。

httpmock.RegisterStubRequest(
    httpmock.NewStubRequest(
        "POST",
        "https://api.line.me/v2/bot/message/reply",
        httpmock.NewStringResponder(200, "{}"),
    ),
)

以降、指定したURLに対するリクエストにはステータスコード200、ボディに"{}"が返るようになります。なお、URLは完全一致で、URLパラメタ(?key=value)も含めて同一の文字列である必要があります。

スタブは、必要なだけいくつでもRegisterStubRequest()で追加できます。

[10/31追記]同一のURLを持つスタブを複数RegisterStubRequest()してもエラーにはなりませんが、そのURLに複数回リクエストを発行しても、常に最初のスタブが使われます。従って、後述のAllStubsCalled()でエラーとして検知されます。

スタブが呼ばれたことを検証する

想定したURLすべてに対して正しくリクエストが送られたことを確認するには、AllStubsCalled()を使います。

if err := httpmock.AllStubsCalled(); err != nil {
    t.Errorf("Not all stubs were called: %s", err)
}

定義したのに呼ばれていないスタブがひとつでも存在するとErrorを返してくれます。呼ばれていないスタブが複数あるときにはErrorに全て列挙してくれます。

なお、リクエストの内容(たとえばLINEに送信したText Messageの内容)まで検証したければ、スタブのNewStubRequest()の第3引数に直接無名関数を書くことで検証が可能です(httpmock - GoDocに例があります)。

その他のTips

Goのテストではあたりまえのことかも知れませんが、その他、知ったこと。

Error()とFail()

  • t.Error()では、テストコードの実行は継続されるが、テストはFAILする。構造体メンバの検証、パラメタライズドテストなど、一度の実行で問題点を全て知りたいときに有用
  • t.Fail()は、テストがFAILし、テストコードの実行が中断される。テストフィクスチャ構築中や、その他、継続しても仕方のない箇所でのエラーに使う
  • t.Log()で出力したログは、テストがFAILしないと出力されない

-run オプション

  • $ go test -run 関数名とすると、関数名に一致するテストだけ実行できる

TODO

今後なんとかしたいこと。

Task Queueのテスト

Task Queueに関するテストが書けない(taskqueue.Add()されたことを検証できない)件。

  • taskqueueをモックすればできるのかも?
  • そもそも、このBOTで個々の処理をTQにする必要はなかったのでは。goroutineを使うべき?

バージョン番号の埋め込み

『みんなのGo言語』p.56に書かれていた、ビルド時の-ldflagsgit describe --tagsで取得したバージョン番号を埋め込む方法を試しましたが、GAE/Goでは指定できず。goapp deploy時に指定しても無駄でした。

とりあえず、Makefile中でversion.goファイルを書き出すことで実現。

依存パッケージ管理

Glideで依存パッケージ管理をしようと試みましたが、deploy時にエラーが出てしまい断念。下記エントリに従えばできそうなので、いつかリトライ予定。

静的解析

gometalinterをローカルおよびTravis CI上でもafter_successで実行していますが不完全。

  • warningだけでも終了コード>0が返るため、scriptで実行させられないのが現状。warningを全部取るか、細かくパラメタ設定するか
  • Travis CI上では、ローカルでは出ないerrorが出る。正しくvendoringする必要がありそう

参考

関連エントリ

書籍

みんなのGo言語[現場で使える実践テクニック]

みんなのGo言語[現場で使える実践テクニック]

第6章 Goのテストに関するツールセット

システムテスト自動化 標準ガイド (CodeZine BOOKS)

システムテスト自動化 標準ガイド (CodeZine BOOKS)

  • 作者: Mark Fewster,Dorothy Graham,テスト自動化研究会,伊藤望,玉川紘子,長谷川孝二,きょん,鈴木一裕,太田健一郎,森龍二,近江久美子,永田敦,吉村好廣,板垣真太郎,浦山さつき,井芹洋輝,松木晋祐,長田学,早川隆治
  • 出版社/メーカー: 翔泳社
  • 発売日: 2014/12/16
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (3件) を見る
第14章 CI(継続的インテグレーション

*1:panic: appengine: NewContext passed an unknown http.Request

*2:ログ出力のためだけにContextを渡しているのであれば考え直したほうがよさそう。テスト実行時ならfmt.Print()でも視認できるので

*3:ローカル開発サーバの生き残りプロセスは $ ps -ef | grep dev_appserver.py で確認できます

*4:調整さんリマインダBOTではこのあたりキレイに書けておらず、いずれリファクタリングします。不慣れな言語こそTDDしないとダメですね

*5:クライアント側でSignatureを検証する必要があり、その機能までSDKに実装されているため

LINEの新しいMessaging APIを試してみた

先日開催されたLINE DEVELOPER DAY 2016で発表された*1BOT APIに替わるMessaging APIを試してみました。

business.line.me

まず、先日作成した「調整さんリマインダBOT」をMessaging APIに移行。ついでに、新機能であるTemplate Messageと、グループやトークルームにBOTを参加させたときの振る舞いを確認します。

nowsprinting.hatenablog.com

Messaging APIへの移行

BOT用アカウントの作成

BOT API Trialのアカウントは利用できないので、新規にビジネスアカウントを開設します。手順は以下の通り。

  1. LINE Business Center -> アカウントリスト -> ビジネスアカウントを作成する
  2. 作成したアカウントの LINE@ MANAGER -> アカウント設定 -> Bot設定
  3. APIを利用する”を選択(この時点でアカウントは、LINE@アプリ、1:1トーク、お店トークが利用できなくなります)
  4. 必要に応じてBotの設定を行なう
    • Webhook送信(デフォルトは"利用しない”)
    • グループトーク参加(デフォルトは"利用しない”)
    • 自動応答メッセージ(管理画面で設定。デフォルトは”利用する”)
    • 友だち追加時あいさつ(同上)

自動応答メッセージおよび友だち追加時あいさつは"利用する"であっても、ちゃんとWebhookが飛びます。メッセージの内容は「メッセージ」から変更できます。

その他、LINE@ MANAGERでBOTのアカウントページなども設定できます。

App Engineアプリケーションの作成

App Engineアプリケーションは(version指定するなどして)使いまわしても良かったのですが、今回は新規に作成しました。

先の記事と同様にWebhook URLを設定し、Channel SecretChannel Access Token*2の取得を行ないます。Channel IDおよびChannel MIDは不要になりました。

BOT SDKの更新

Golang用のSDKもアップデートされているので、下記コマンドで更新しておきます。

$ goapp get -u github.com/line/line-bot-sdk-go/linebot

BOT本体の変更

主な変更点は以下の通り。

  • LINE BOTの初期化において、Channel IDおよびMIDが不要になったので削除
  • 代わりにACCESS_TOKENが必要になったので追加
  • linebot.OpTypeがなくなったので、stringに変更
  • OpTypeの判定は全てlinebot.EventTypeに変更。ここの判定はかなりスッキリした
  • APIの変更に対応
    • 大きな変更点として、ユーザの発言に応答するReply Messageと、BOTが起点となって発信するPush Messageに二分されました。今回のBOTは仕様上Pushを利用しましたが、通常のBOTではReplyを使っていくことになるでしょう。
  • テストの修正
    • X-Line-Signatureヘッダキーの変更(もとはX-LINE-ChannelSignature)
    • 友だち追加テストデータ(json)のフォーマット変更

以上でテストを通し、goapp deployして動作確認を行ないました。詳細・差分はGitHubに置いてあるソースを見てください。

github.com

なお、旧BOT APIで取得できていたユーザの識別子であるMIDは、Messaging APIで取得できるUserIDとは異なります。 先頭文字はユーザはU、グループはCトークルームはR。+英数32文字(16進数?)という書式になっていました。

Template Message

新機能のTemplate Messageは、トーク上に複数のボタンなどで構成されるテンプレートを表示し、ユーザの応答を得るための仕組みです。Buttons, Confirm, Carouselの3種類を利用できます。

現時点でいくつか制約があるようです。気づいたものが以下。

  • iOS版のLINEアプリ(バージョン 6.6.2)では、Template Messageは「LINEアプリのバージョンが古いため(ry」と表示され、利用できません
    • バージョン 6.7.0で対応されました
  • Android版のLINEアプリ(バージョン 6.7.0)では利用できました
    • バージョン 6.5.1では「ご利用のバージョンでは対応していないか不正なURLです」と表示され、利用できませんでした
  • ButtonsおよびCarouselにおいて、APIリファレンスにはthumbnailImageUrlが省略可能と書かれていますが、省略するとエラー*3が返ります*4
  • [11/2追記]Template Messageに設定するaltTextは「非対応端末で表示される代替テキスト」と説明されていますが、Notificationおよびトーク一覧画面での表示にも使われています(クライアントバージョン 6.8.0時点)

グループおよびトークルーム

従来のBOTはユーザと1:1のトークしかできませんでしたが、Messaging APIでは、グループおよびトークルームへの招待が可能となりました。

こちらも現時点でいくつか制約があるようです。気づいたものが以下。

  • グループに複数のBOTを招待できない。2つ目以降のBOTは「招待中」のまま保留され、Webhookのjoinも飛ばない
  • トークルームは、招待したタイミングではjoinは飛ばない。招待後、最初のメッセージがWebhookで送られる直前にjoinが投げられる
  • トークルームにも複数のBOTを招待できない。2つ目のBOTを招待すると、即退出される(元々いたBOTが退出するケース、2つ目が退出するケースの両方を観測)
  • トークルームのメンバーはキックできない仕様なので、BOTは自らLeaveAPIで退出しない限りルームに一人取り残される。ルームの参加人数を知ることもできないので、取り残されたら抜ける手段はなさそう
  • グループおよびルームからのWebhookでは、そのメッセージがどのユーザの発言かを知ることはできない(送信元はGroupIdおよびRoomIdとなる)。Template MessageからのPostback Eventも同様。
  • ユーザと1:1のトークでは、ユーザの表示名などはGet ProfileAPIで取得できるが、グループおよびトークルームのメッセージ送信元(GroupIdおよびRoomId)の情報を得ることはできない
  • BOTの送信したメッセージは、BOTには送信されることはない(オウム返しするBOTで、無限ループを気にする必要はない)
  • [10/30追記]すでにBOTがグループに所属している状態で他のユーザを招待すると、再びWebhookでjoinイベントが送信される。タイミングは「参加」時ではなく「招待」時点。意図がわからない。

参考

*1:参加したわけでなく、LINE LIVEで見てました。Beacon欲しかった…

*2:Tokenは、横の[ISSUE]ボタンをクリックすると発行されます

*3:linebot: APIError 400 A message (messages[0]) in the request body is invalid

*4:SDKでなくLINEのバックエンドの仕様のようですが、どこに申告すればいいのでしょうか?

調整さんリマインダLINE BOTを作ってみた

調整さん」を(日程調整ではなく)出欠の管理に使っている前提で、出欠登録のリマインダをLINE BOTとして作ってみたので、そのメモ。

[9/29追記]LINE BOT APIはDeprecatedとなり、Messaging APIに置き換わりました。同時にSDKもMessaging API対応のものに更新されていますので、ご注意ください。

[10/23追記]LINE BOT API Trial Accountは、11/16に完全削除を予定されていると発表されました。

新しいMessaging APIへの置き換えについては、次のエントリを参照してください。

nowsprinting.hatenablog.com

環境

仕様

  • 調整さんのイベントをクロールし、3日後に開催される予定がある場合、参加人数などを通知する
  • 通知は、BOTと友だち登録した人全員に送られる(cronで毎朝8:00に起動)
    • LINEグループに参加させたかったが、BOTはLINEグループに参加させることはできない*1
    • BOTの友だち登録は、デフォルトでは50人まで
    • BOTはid検索できないため、友だち登録は次の手順で行なう:QRコード画像を送る→画像を端末に保存→LINEで[友だち追加]→[QRコード]→[ライブラリ]→保存したQRコード画像を選択
  • 監視対象の調整さんイベントは、ハッシュをapp.yamlに記述しておく
  • LINE BOTのシークレットキーなどもapp.yamlに記述

GAE/Goのプロジェクト作成

Google Cloud Platformコンソールで新規プロジェクトを作成。プロジェクトIDを確定させる。

LINE BOT(チャンネル)の開設・設定

  • BOT API Trial Accountを開設
  • 開設したチャンネルの"Basic Information"にある"Callback URL"に、AppEngine側のコールバックを受け取るURLを設定
    • "https:// YOUR_PROJECT_ID .appspot.com:443/line/callback"
  • "Server IP Whitelist"は設定しない。設定するとAppEngineのIPアドレス変動に対応する必要があるため

必要なパッケージのインストール

$ goapp get github.com/line/line-bot-sdk-go/linebot
$ goapp get golang.org/x/text/encoding/japanese
$ goapp get golang.org/x/text/transform

LINE BOTの基本部分を実装

LINEからのコールバック用のハンドラをfunc init()に定義する。パスは上で定義したもの。

func init() {
    http.HandleFunc("/line/callback", lineCallback)
}

lineCallback()の中身は、ほぼBOT SDKのサンプルをコピペ。ただし、App Engineではhttp.Clientとしてappengine/urlfetchを使用する必要があるため、LINE BOT Clientからもこれを使うように、下記のように初期化する。

bot, err := linebot.NewClient(channelID, channelSecret, channelMID,
                        linebot.WithHTTPClient(urlfetch.Client(c)))

ここで一度goapp deployして動作確認。BOTを友達登録して、トークで送った内容をオウム返ししてくれればok.

友だち登録時にMIDを取得してデータストアに格納する

LINEへのメッセージ送信には、toにMID(LINEユーザの固有ID)を指定する必要がある。友だちに一斉配信するためには、あらかじめ友だち登録時に相手のMIDを取得しデータストアに保存する。

友だち登録および解除は、メッセージとは別フォーマットのオペレーション・イベントが通知される。パラメタのOpTypeによって、追加(ブロック解除も等価)、削除を判別できる。

なお、メッセージとは送信者のMIDが格納されている位置が異なる。OperationContent.Params[0]に入っているので注意。

    if content.IsOperation {
        //オペレーションイベント受信
        opContent, err := content.OperationContent()
        if content.OpType == linebot.OpTypeAddedAsFriend {
            task := taskqueue.NewPOSTTask("/task/addfriend", url.Values{
                "mid": {opContent.Params[0]},
            })
            taskqueue.Add(c, task, "default")
        } else if content.OpType == linebot.OpTypeBlocked {
            task := taskqueue.NewPOSTTask("/task/removefriend", url.Values{
                "mid": {opContent.Params[0]},
            })
            taskqueue.Add(c, task, "default")
        }
    } else if content.IsMessage && content.ContentType == linebot.ContentTypeText {
        //テキストメッセージ受信
        (snip)
    }

Task Queue/task/addfriendは送信者のMIDをデータストアに保存、/task/removefriendは送信者のMIDをデータストアから削除するもの。

メッセージを全員に送信

テキストメッセージを受信したとき処理を、送信者にオウム返しするもの(サンプルの実装)から、データストアに保存されている全MIDへの送信に変更する。

//データストアから購読者のMIDを取得
q := datastore.NewQuery("Subscriber")
var subscribers []Subscriber
if _, err := q.GetAll(c, &subscribers); err != nil {
    return err
}
mids := make([]string, len(subscribers))
for i, current := range subscribers {
    mids[i] = current.MID
}

//全員に送信
bot.SendText(mids, message)

調整さんリマインダ処理

調整さんリマインダは、下記処理をcronで毎朝起動されるようにする。

調整さんのイベントをクロール

cronからキックされるハンドラをfunc init()に追加する。

http.HandleFunc("/cron/crawlchouseisan", crawlChouseisan)

以下、crawlChouseisan()の実装。

調整さんのAPIは公開されていないが、イベント作成者にのみ表示される「出欠表をダウンロード」リンクのURLから、csv形式のレスポンスが得られるのでこれを利用する。

調整さんのイベントハッシュは、イベント作成時にコピーしておき、app.yamlに定義したものを取得する。

url := "https://chouseisan.com/schedule/List/createCsv?h=" + os.Getenv("CHOUSEISAN_EVENT_HASH")
c := appengine.NewContext(r)
client := urlfetch.Client(c)
res, err := client.Get(url)

csvをパースする。MS932なのでjapanese.ShiftJISデコーダを使用している。

また、encoding/csvReaderは、1レコード目と異なるカラム数のレコードをエラーと判断する。調整さんのcsvは1レコード目がタイトルのみとなっており、素直に読むとデータ行で"wrong number of fields in line"というエラーメッセージが出てしまうので、下記にようにErrFieldCountは無視するようにした。

reader := csv.NewReader(transform.NewReader(csvBody, japanese.ShiftJIS.NewDecoder()))
for {
    row, err := reader.Read()
    if err == io.EOF {
        break
    } else if e2, ok := err.(*csv.ParseError); ok && e2.Err == csv.ErrFieldCount {
        //フィールド数エラーは無視
    } else if err != nil {
        log.Errorf(c, "Read chouseisan's csv failed. err: %v", err)
        return nil
    }
    (snip)

日付などのパース処理は割愛。日付をキーにしたMapに詰める。

3日後の予定があれば通知

AddDate()で3日後を指すDateを作り、Mapにその日のValueが存在すれば通知対象とする。なお、App Engine実行環境ではUTCが使われるため、JSTを明示的に使用する。

tz, _ := time.LoadLocation("Asia/Tokyo")
today := time.Now().In(tz)
(snip)

//3日後の予定をピック
after3days := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, tz).AddDate(0, 0, 3)
obj, exist := m[after3days.String()]
obj, exist := m[after3days]
if !exist {
    log.Infof(c, "Not found schedule at 3 days after.")
    return
}

//メッセージを組み立てて送信
(snip)

cron.yamlを作成

作成したハンドラを、毎朝8:00 JSTに起動されるようにcronを定義。

cron:
- description: crawl chouseisan every day
  url: /cron/crawlchouseisan
  schedule: every day 08:00
  timezone: Asia/Tokyo

以上をデプロイして、今のところ想定通り動作している模様。

途中、シネマカリテで『不思議惑星キン・ザ・ザ』<デジタル・リマスター版>を観た影響で、ひたすら「クー」と返す "キン・ザ・ザBOT" に作り変えたくなったけど、よく我慢した。

ソース

下記リポジトリで公開しています(Apache License 2.0)

GitHub - nowsprinting/ChouseisanReminder: Reminder line-bot for chouseisan.com

はじめてのGoでありAppEngine/Goアプリなので、作法とかなっとらん自覚はあります。なので参考程度に。『みんなのGo言語』ポチったので届いたら読んで改善したい。

参考

*1:りんなはグループに登録できるので、トライアルの仕様?

Unite 2016 Tokyo に行ってきました #unite2016tokyo

Unite 2016 Tokyoに行ってきました。去年はVR系セッション優先で聴講しましたが、今年はVR、最適化、絵づくりあたりを満遍なく。

講演資料は下記ページで即日公開、動画も近々公開されるということで、印象深かったメモのみ。

Unity - Unite 2016 Tokyo 講演ガイド

DAY 0

トレーニングデイのコース A「Unity サービス実装ワークショップ」に参加してきました。Unityのクラウドサービスである Cloud Build, Analytics, In-App Purchase, Adsなどの体験ワークショップ。

Cloud Build以外は使っていなかったのですが、AnalyticsとGame Performance*1は適用も簡単で、早速自社プロダクトに組み込んでFabric*2と比べてみようかと。

このワークショップでは、教材のプロジェクトだけでなく、UnityのインストーラWin32/64/MacOSX)まで入ったUSBを配布され、プロジェクトはごく最小限のエディタ操作とコピペで動作、さらにはCloud Buildの体験にはGitHub上のプロジェクトをforkするだけと、かなり至れり尽くせりの(安全サイドに倒した)ワークショップでした。

この手のワークショップ、これまで主催側として上手く行った例がないのですが、やはりこれくらい準備しないとダメですね。見習いたい。

DAY 1

モバイル端末向けのUnityアプリケーションの最適化実践テクニック

  • 最適化の前に、良いデータを計測することが大事。各種プロファイリングツールの紹介、使いかた
  • テクスチャ、モデル、オーディオの最適化
  • メモリのフラグメンテーションに注意する*3
    • boxing, foreach*4, 文字列操作に注意
    • JSON操作は、Unity 5.3以降ならJsonUtilityを使うこと
  • PrefabやCanvasを適切に分割する

実践!Oculus Rift - VR開発テクニック

  • Oculus RiftおよびGear VR向けアプリをストアに掲載する申請ができる
  • バイナリは、Store/RC/Beta/Alphaの4種類(4フェーズ)アップロードできる
  • カテゴリにある「先行アクセス」はBeta版などに使える。「コンセプト」はフルゲームでないアプリでも申請可能。
  • 必要なストア画像などはOculus Store Art Guidelineを参照
  • 価格もOculusで審査される。法外な値段はリジェクトされる
  • VR酔いの程度を三段階にレーティングされる

ハードウェア性能を引き出して60fpsを実現するプログラミング・テクニック

DAY 2

ホンモノ志向のVR空間づくり

  • 3Dをどう見ているか
  • リアル世界で生活して経験を積んでいるので、写真でも立体や遠近感を得られている
    • 知っているものがあるとスケールがわかり、距離もわかる。道路標識とか
    • 線で描かれた立方体を、立方体だとわかる
    • この感覚は、13歳くらいで習得できるらしい
  • 静止画ではわかりにくくても、動いているとわかる
  • Oculus Frameworkの"Teleportation"は参考になる
  • 黄金比、フィボナッチ、Rule of 3rd。科学的根拠はない
  • Light/Shadow
  • VRコンテンツを作るとき、単眼でテストする。それで十分立体感を得られるならば、二眼にすればよりよく見えるはず

映像制作のゲームチェンジャー:メイキング オブ ”THE GIFT”

  • カラーボールのシーンは、頂点数がIntegerの最大値を超えてマイナス表記になった
  • ボールは複数あわせて1メッシュにしている
  • ボールのポリゴンは実は粗い。少し小さめの円形に切り取るシェーダで球体に見せている
  • デザイナさんがエディタで作業できるレベルで動く

marza-realtime.com

Fate/Grand Orderにおける、ディライトワークス流Unity活用術

  • バトルキャラはビルボード。槍など横に振り回すもの(奥行き表現があるもの)は3D。Mayaで作ってfbxエクスポート。
  • エフェクトなどMayaでは表現が難しい物はUnity側でエフェクトをPrefabにしてぶら下げる
  • モーションはPlayMaker。カメラ移動、エフェクトの再生もここで。キャラ・モーションが増えたときもActionの追加で済ませる(アプリ更新を避けられる)
  • 宝具(必殺技)演出はuSequencer(カットシーンエディタ)で作成
  • ゲーム開発環境とは別環境の「宝具制作環境」。実行ボタンを押すと宝具が再生される。デザイナが素早く開発できる
  • Unityエンジニア=プランナーでもデザイナーでもプログラマでもない。今後増えてくる役割かも?

マルチシーン編集の使い方

  • Unity 5.3から
  • シーンを並べるほか、レイヤのような使いかたで編集の競合を避けられる
  • ヒエラルキにシーンをD&Dすれば追加で開く
  • 新しいSceneManeger API

Unityとアセットツールで学ぶ「絵づくり」の基礎(ライト、シェーダー、イメージエフェクト)

  • Unity 4までの絵づくり、Unity 5からの絵づくり
  • ぼくのUnityと違う
  • 「色を塗る」のではなく、「光を反射させて色を出す」。要素は、マテリアル×ライト×カメラ
  • 間接光

Making of The Modern Zombie Taxi Co.

  • オブジェクトの選択は、Look at Button (Gazing) が最適
  • 入口をリビングルームにした。プレイヤーをVRに慣れさせる空間
  • ダッシュボードに表示する情報は、通常のゲームより少なくしないとノイズにしかならない。当初11項目を2項目に減らした
  • 道案内も工夫。VR空間内を見てもらえるように、オブジェクトとして配置。
  • チュートリアルも難しい
    • VR経験のない人は、VR世界に興奮する。ゲームを遊ぶことを忘れる。
    • シンプルに、最低限に、同じ情報を繰り返し提示する。
  • ほか、VRでやってはダメなことも試してみた結果なども紹介されていて面白い

所感

トレーニングデイも、懇親会も、そして講演も、楽しめました。 今年から導入された有料S席システムも利用しましたが、聞きたい講演を確実に、しかも並ばず入れるのは快適。

最近よく言われる、講演中のシャッター音問題も、各回開始前アナウンスの徹底の効果なのか、スライド全ページ撮影するような人は見当たらず。ほかのカンファレンスもこうなるといいですね。

関係者、登壇者の方々、ありがとうございました!

*1:クラッシュログを採取してくれるサービス。BetaなのでProライセンスが必要

*2:先月Unity SDKがリリースされた。AnswersとCrashlyticsがそれぞれAnalyticsとGame Performanceに競合

*3:この話題は携帯電話Javaやってた頃を思い出す

*4:arrayをヒープにコピーしてしまう

#tryswiftconf 3日目のテスト系セッションまとめ

3/3〜5の3日間、サイバーエージェントさんのセミナールームで開催されたtry! Swift、その3日目に行ってきました。 海外からも100人を超える方々が参加され、とても活気のあるカンファレンスでした。

Swift言語にフォーカスした本カンファレンスですが、3日目は開発者テストに関するセッションが2つありました。圧倒的にスイフト力が足りない私なので、これらのセッションについてだけ書きます。

Swiftにおける実践的なモック化について

Veronicaさんのセッション。Objective-C動的言語なので、テストコードでモック(を含む、テストダブル)を使ってプロダクトコードやフレームワークの挙動をランタイムに置き換えることができていました。またそれを容易に実現するためのOCMockというライブラリもありました。

しかしSwiftではこれができないため(反面、安全と言えます)、どうモックを使っていくべきか?というセッションです。

そもそも、モックを使う理由

続いて、DI(Dependency Injection: 依存性の注入)を使う理由

  • カスタマイズ(置き換え)が容易にできる
  • オーナーシップが明確になる
  • テスタビリティ(試験性、テスト容易性)を上げる

テストダブルの代表的なもの

  • スタブ(メソッドの戻り値を置き換える)
  • モック(メソッドの呼び出し、引数を検証する)
  • パーシャル・モック(特定のメソッドだけ置き換え。これは『xUnit Test Patterns』の定義・分類ではなく、恐らくOCMock独自の言葉)

パーシャル・モックのアンチパターン

  • setUp()でモックを定義するのが大変になる
  • What's real? What's fake?

モックを自分で作る場合、本物のサブクラスを作るのでなく、そもそも必要なものをProtocolとして定義しておく。Javaでも原則Interfaceを書くという宗派がありましたが、それ*1

「ロールをモックせよ」の話。むやみにモック化しない。壊れやすいテストになる。

Swiftで将来的に使えるようになりそうな?モックフレームワークの紹介(リンクはぐぐったもの)

GitHub - rheinfabrik/Dobby: Swift helpers for mocking and stubbing

GitHub - DeliciousRaspberryPi/MockFive: A Mocking Framework for Swift Unit Tests

GitHub - mflint/SwiftMock: A mocking framework for Swift

GitHub - SwiftKit/Cuckoo: First boilerplate-free mocking framework for Swift!

CuckooはMockitoインスパイア系っぽい。

QA

  • 日付や時間に依存するテストはどう書くべき? -> 遭遇したことがないけど、考えてみるのも面白そう

@niwatakoさんの書き起こしも参照。

niwatako.hatenablog.jp

所感

Objective-Cでは(OCMock/OCMockitoでは)依存オブジェクトの扱いが雑な設計であっても、ランタイムにモック化することで強引にテスト可能だったりしたのですが、Swiftではちゃんと設計しないとダメだよね、という話。

またSwiftに限らず、モックは便利さにつられて乱用しがち、そしてフラジャイルな(壊れやすい)テストになりがち、という点まで、短い時間で正しく警告されていてすばらしいセッションでした。

モックに頼らない、という点は、設計の見直しで回避できるものももちろんありますが限界もあるので、より上層の(結合度の高い)テストレベルで担保するなどが最適解かな、と考えています*2。このセッションではそこまで詰め込めなかっただけだと思う。

なお、Swiftの一部メソッドを(Objective-Cの機能を使って)動的に置き換える手段について、Danielさんの"Code Injection from scratch"セッションで触れられていました。こちらも@niwatakoさんの書き起こし参照。

niwatako.hatenablog.jp

An Artsy Testing Tour

Ashさんのセッション。Artsyではこれまで4つのアプリに対してテストを書いてきたが、それぞれアプローチが違うので紹介する。

前提として、

ひとつ目のアプリ

  • テストは無かった。Apple TVローンチまで時間がなく、担当者一人で作ったもの。
  • テストを書くべきか否か、バランスを判断。コードベースが小さいものだったので、ローンチを優先した。

ふたつ目のアプリ

  • はじめテストは無かったが、コードベースが大きかったのでリグレッションテスト強化のため、後からテストを追加した。
  • "Bus factor"*3防止のため、テストのドキュメントとしての役割を重視*4
  • DIを多く使った
  • RSpec*5を使い、セットアップの共通しているテストはbeforeEachにまとめるなど、テストコードをリファクタリング。リーダブルなテストに。
  • 余りにネストしたcontext*6はおかしいので、設計を見直す。十分に説明的な名前がつけられるか?
  • テストはドキュメント
  • 振る舞いをテストする。コードをテストするのではない。

みっつ目のアプリ

  • これもはじめテスト無し、後で追加した。
  • ネットワークアクセスのあるアプリで、開発者も多く実装も散らばっていた。
  • はじめiPhone用、後にUniversal Appに。コンテキストが異なるので、一揃いのテストスイートを、iPhoneiPadふたつの観点からテストを実施した。
  • 大きなクラスからテストしたが、修正が入ると既存テストの書き換えも多発した。
  • Snapshot Test。画面のスナップショットを撮ってpngで保存し、ピクセルベースでdiffを取り合否判定*7。pull-requestの中にスクリーンショットがあるのでレビューしやすい。
  • 小さいクラスからテストすべき
  • 追加したコードにはテストを書く

よっつ目のアプリ

  • これは最初からテストを書いた。はじめてのSwiftアプリで手探り状態のところ、テストコードが拠り所になった。
  • テストツールにはQuick*8を使用。
  • Nimble*9というMatcherライブラリを使えば、assert()を、expect().to()形式で書けて可読性が高い。
    • Arrayなども直接比較できる。拡張もできるので、Snapshot用のMatcherを作った。

QA

  • TDDはハイコスト。クライアント側は手を抜いていいのでは?という意見があるが -> Swiftは型が強力なのでLLと比べてテストの重要性は低いが、でもコンパイラだけではチェックできない不具合を見つけられる。ドキュメントの価値もある。テスト大事。
  • Snapshot Testはどのテストレベルでやっている? -> 決めかねている。End to Endでなくてもいい。
  • TDDで「機能」を捉えるのが難しい -> TDDは賛否両論。Ashさんはどちらでもなくケース・バイ・ケース。繰り返しやっているとテスタブルなコードを書けるようになる。ロバストにはなりきらなくても、価値はある。小さく、小分けして、DI。繰り返しやってみること。練習である。

QAのメモはかなり怪しい。@niwatakoさんの書き起こしも参照。

niwatako.hatenablog.jp

所感

テストに対する姿勢、メリットが明確で、かつ4パターンの実際のアプリの事例を挙げられていることもあって、とても説得力のある、良いセッションでした。Spec BDD派が増えそう。

Spec BDDに限らず、可読性の高いテストはドキュメントとしても有効なので、ちゃんとメンテナンス、リファクタリングすべきですね。

また、質疑応答で出たTDDの話。プロダクトコードとテストコードを行き来することで、複数の視点でコードを見ることができるようになるはずなので、訓練だと思ってTDDをやってみるのは良いことだと思う。 後からテストを書くのは難易度が高いことが多いので、テストを書く習慣、テスタブルなコードを書く習慣をつけるのは良いこと。

カンファレンス全体を通して

先日のDroidKaigiもそうですが、開発者主体で、スポンサーも付けて、有料で、海外からも参加者およびスピーカーが来てくれる。ほんの2〜3年前には考えられなかったことで、それを実現された主催者・関係者の尽力はすごいと思います。

改めて、お疲れ様でした。ありがとうございました。

こうして(ごく一部のセッションの)ブログを書くことくらいしかできませんが、少しでも盛り上がりに貢献して、次回以降につながればいいな、と。

最後に蛇足ながら。Twitterで「本カンファレンスが1スレッド進行なのが良かった」という話がありました。完全に同意!なのですが、それは今の時期のSwiftだから、という条件付きかも。マルチセッションがすべて悪、みたいな方向には行かないで欲しいかな。

*1:Javaでは「C言語のヘッダファイルみたいに増える」という批判がありましたが、少し前までObjective-Cで書いていたから抵抗ないはず?

*2:モック愛好家のポジショントーク注意

*3:日本だとトラックを使いますよね?

*4:いわゆる仕様化テスト

*5:と言っていたけど恐らくKiwiかな?

*6:describeと同義

*7:FacebookOSSとのことなので、恐らく https://github.com/facebook/ios-snapshot-test-case を使用

*8:RSpec, Kiwiと同様、Spec BDDライブラリ。 https://github.com/Quick/Quick

*9:https://github.com/Quick/Nimble

Mastering Android NDK(PACKT)の査読をした話

Packt Publishing Ltd. から出版された『Mastering Android NDK』の査読をお手伝いさせていただきましたのでご紹介。

f:id:nowsprinting:20151212122627p:plain

PACKTでeBook版を購入すると、PDF、ePub、Mobi*1でダウンロードできるほか、Kindleのコレクションに直接送る(send-to-kindle)こともできます。 Print版(ペーパーバック)もキレイな作りです。

www.packtpub.com

もしくは、Amazonでも購入できます。

www.amazon.co.jp

サンプルコードはPACKTのライブラリからダウンロードできますが、同じものがGitHubにもあります。

github.com

本書の内容・想定読者

本書は、Android NDKを使って、Android SDKだけでは実現困難なグラフィックスやサウンドの実装方法を紹介し、最終章でゲームを一本完成させる、という構成です。

Chapter 1でNDKアプリのビルド、Chapter 2でよく使われるC++ライブラリの組み込み方法を紹介した後、以降Chapter 10までC++の本。UnityやUnreal Engineといったゲームエンジンの台頭により、「クロスプラットフォームのゲームをAndroid NDKで作る」というケースは減っていると思いますが、もっと下のレイヤーまで知りたい、C++を駆使して実装したい、という方には参考になるはずです。

参考までに、目次は以下の通りです。

  • Chapter 1: Using Command-line Tools
  • Chapter 2: Native Libraries
  • Chapter 3: Networking
  • Chapter 4: Organizing a Virtual Filesystem
  • Chapter 5: Cross-platform Audio Streaming
  • Chapter 6: OpenGL ES 3.1 and Cross-platform Rendering
  • Chapter 7: Cross-platform UI and Input System
  • Chapter 8: Writing a Rendering Engine
  • Chapter 9: Implementing Game Logic
  • Chapter 10: Writing Asteroids Game

査読を頼まれた経緯

私のまわりでPACKTの査読をしたという話を聞いていないので、経緯などをざっくりと。

日本では、著者が自身の知り合いに査読を依頼し、その査読反映をもって著者脱稿として編集者に渡る、という流れが一般的だと思いますが、PACKTでは以下の流れで進められました。

  1. PACKTの「レビュアー獲得責任者」が、本書の査読ができそうな人を探して打診する。GitHubリポジトリなどの実績から選んでいるそうです
  2. 原稿は、著者とも編集者とも直接やり取りはせず、本書のコーディネータ経由で送られてくる(まとめてではなく、章ごとに数週間間隔で)
  3. 受け取った原稿に対し、テクニカルな指摘のほか、章ごとに内容の過不足や満足度といったアンケートも書いて返送する

本書に関しては、Android NDKやGradleまわりのサンプルをGitHubに上げていたのが目に止まったのだと思います*2。ニッチなものでも(ものこそ?)公開しておくものですね。

英語は義務教育レベルも怪しい私ですが、文法や言い回しに関してはちゃんとPACKTの編集者さんが付いており、我々レビュアーはテクニカルなところだけ見てくれればいい、ということでお受けしました。 とは言え、テクニカルなところも、オーディオとか門外漢なので余りお役に立てていないのですが。

なお、PACKTの書籍をお持ちの方はご存知だと思いますが、Reviewerは紹介文付きで載ります。

f:id:nowsprinting:20151212115359j:plain

「ぜひ書店でお手にとってー」と言えないのが洋書の辛いところですが、GitHubに上がっているサンプルコードを見て良さそうなら本も買う、という判断がいいように思います。

また、Print版を持っていますので、勉強会などでご一緒するときに事前に言って頂ければ持参し、お見せすることはできます。

*1:Kindle向けフォーマット

*2:噂のトニーモリス氏からはメール来てません