woshidan's blog

あいとゆうきとITと、とっておきの話。

Cocos2d-xの勉強会のスライドを一通り辿ってみたときの話

この記事は〇〇勉強してみた Advent Calendar 2017の19日目の記事です。

はじめに

現在お仕事で開発しているライブラリはiOS/AndroidだけでなくCocos2d-xやUnityなどのプラットフォームに対し、クロスプラットフォーム展開をしており、その一環としてCocos2d-x用SDKのメンテナンスを担当したことがありました*1

それがきっかけで上長から「(Cocos2d-xの)勉強会とかもうちょっと行ってみたら? まとまった時間は取れるかどうかわからないけど」という話になり、日々30分強をもぎとり*2、過去の勉強会のスライドを30個眺めてみたときの話をします。

なぜ過去の勉強会のスライドを眺めていったかというと、インターネット上に東京から出かけられる範囲で開催予定の勉強会が見つからなかったからです*3

やってみた感想として

自分には普段からあまり馴染みのない分野について調べる場合、説明の文章を読むだけで頭がヒートアップしてなかなか進まないことが非常に多いです。そのため、慣れない分野に取り組むとき、普段は2時間くらいを単位として時間を確保して取り組むことが基本だったのですが、今回はお仕事の都合で30分強しか枠が取れないので果たしてどうなるかなという感じでした。

結論として、自信を持ってコードを書いていける強度の知識はあまり身についておらず、実装関連も細切れの時間だとなかなか取り組むことができずで中途半端な感じでした。

その代わりに馴染みのような感覚ができました。以前はCocos2d-xの関連issueを読むとき、かなり多くの文章が初めて読む文章だったのですが、これだけ読んでみると、どれもなくなんとなく知ってるものを詳しく調べるの枠に入ってきます。

後半にあたった資料は前半にあたった資料より時間あたりに(がっつり学習したほどではないにせよ)得られるものが多かったです。

なんとなく知ってるというのはそのまま実装に使えるわけではありませんが、調査とか調査とか調査とかばっかりしていると、なんとなく知ってるものは調べるに当たってゆるやかでも時間の計算ができるので非常にありがたいです。精神に効きます。

また、これまで学習・即・仕事、みたいな一直線でかつかつの学習計画をとることが多かったため、日頃からなんとなくアンテナを張っておけ、という話がどの程度有用なのかよくわかりませんでしたが、なるほどこういう感覚を作るためのものなのだなぁ、と体感できて面白かったです。

最近はリモートワークで通勤なしとはいえ全部の分野に毎日2時間かけるわけにはいかないため、今回得た感覚を生かして、トレンドを把握したい分野、直近で専門的に取り組みたい分野、などを分けてQiitaやTwitterをもっと利用していきたいなと思いました。

言われなくてもわかるやろという話ですが、めっぽう体感派なのですみません...。

得たゆるふわ知識

最後にゆるふわなのであんまり参考にならないのですが、自分が特に印象に残った範囲をつらつら箇条書きにして締めたいと思います。

現場からは以上です。

  • アプリ界隈と用語が異なる
    • サーバ連携 = 通信 / オブジェクト = UI Componentなど。発表者の周りの用語かもしれませんが意外だった。検索するときに戸惑いそう...
  • 物理エンジンの作り方で
    • Chipmunk シェイプを作成は設定項目が多いので専用エディタがある、物理構造の一部を固定 => joint/衝突判定用のリスナ b2ContactListener
    • Box2D AngryBirdとかで使われた.
  • 画像を読み込むときは
    • I/Oを減らすためにTexture Atlas(キャラチップやマップチップのような画像)を利用
    • CCSpriteBatchNodeはアセットの用意が複雑だが圧倒的に速い
    • ラベルの表示は文字をそのままCCLabelTTFで実装するとフォントサイズをOS/機種ごとに変更する必要があってつらいので、CCLabelBMFontクラスで画像化するとよい
  • Cocos2d-x 3.0系以降ではC++11を想定して良いこと
  • リソースなどの調整をするたびにエディタを触って組み込んでビルドするのは大変なのでサーバーに画像などを置きアプリのリソース確認の際にはサーバからリソースを取得して確認を行うという話があったこと
    • それがCocos2d-xのIDEにも組み込まれていたこと(Cocos Code IDEとHot Updating)
  • UIの部品は基本的なボタンが含まれるCCMenuItemの仲間とその他に別れ、後者の方が取得できるイベントが多いが前者の方がイベントリスナなどの設定がまとめてできること
  • アニメーションは、キーフレームに合わせて効果音の設定をし、キーフレームを組み合わせてタイムラインを作って書き出すこと
  • アニメーションなどをGUIで生成するツールは現在開発が活発ではなく不安が残ること
  • 普通のゲームでは毎フレーム呼ばれるupdate()関数で諸々のグラフィックの更新を少しずつするらしいこと
    • cocos2d-xではrunActionを使って書いていくのがCocos流

参考

得たゆるふわ知識、の節で書いた範囲のものについて

*1:そのときはAndroidの方の知識で頑張りました

*2:進捗は一応出してたからいいですよね!

*3:2017年11月当時、これはこれで面白いなと思いました。

adbのCLIでAndroidのエミュレータを起動したりアプリの起動、テストの実行をしたりする

この記事はAndroid Advent Calender その2の15日目の記事です。

今回は

https://developer.android.com/studio/command-line/adb.html https://developer.android.com/studio/run/emulator-commandline.html https://developer.android.com/studio/test/command-line.html

を見て色々素振りしてみようと思います。

シミュレータの起動

/Users/woshidan/Library/Android/sdk/tools/emulator -avd Nexus_5X_API_22

で起動することが可能です。テストを実行させたり、アプリをインストールして起動したりしてみましょう。

アプリのインストール

$ adb install app/build/outputs/apk/app-debug.apk 
app/build/outputs/apk/app-debug.apk: 1 file pushed. 5.8 MB/s (2910562 bytes in 0.477s)
    pkg: /data/local/tmp/app-debug.apk
Success

# 二つ以上シミュレータを起動 or 実機を接続している場合
$ adb devices
List of devices attached
162EJP011A71181614  device
emulator-5554  device

$ adb -s emulator-5554 install app/build/outputs/apk/app-debug.apk 
app/build/outputs/apk/app-debug.apk: 1 file pushed. 142.6 MB/s (2910562 bytes in 0.019s)
    pkg: /data/local/tmp/app-debug.apk
Success

apkはプロジェクトのbuildディレクトリ以下を探せばあると思います。

アプリのアンインストール

$ adb uninstall package
Success

アプリの起動

# アクションがandroid.intent.action.VIEWのIntentに反応するActivityへ暗黙的Intentを送る
$ adb shell am start -a android.intent.action.VIEW
# 特定のActivityへ明示的Intentを送る
$ adb shell am start -n io.test.woshidan/io.test.woshidan.MainActivity

Logcatのログを標準出力に出す

$ adb logcat

操作の様子を録画する

# API19以上の実機かAPI24以上のシミュレータ
$ screenrecord /sdcard/Movies/demo.mp4 

# adb pull で当該ファイルをPCなどに持ってこれる

テストを実行する

ADBを使う

# テストで実行可能なinstrumantationの一覧
$ adb shell pm list instrumentation
instrumentation:com.android.emulator.smoketests/android.support.test.runner.AndroidJUnitRunner (target=com.android.emulator.smoketests)
instrumentation:com.android.smoketest.tests/com.android.smoketest.SmokeTestRunner (target=com.android.smoketest)
instrumentation:com.example.android.apis/.app.LocalSampleInstrumentation (target=com.example.android.apis)
instrumentation:com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner (target=com.example.woshidan.myapplication)
instrumentation:com.example.woshidan.newtestapplication.test/android.support.test.runner.AndroidJUnitRunner (target=com.example.woshidan.newtestapplication)

# プロジェクト全体のAndroidTest
$ adb shell am instrument -w com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner

# AndroidTestのうち特定のクラスのテストを行う
$ adb shell am instrument -w -e class com.example.woshidan.myapplication.test.ExampleTest com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner

gradleを使う

// プロジェクト用のbuild.gradle
buildscript {
    repositories {
        google() // gradlewでgoogle()リポジトリに探しにいくためにはここに追加する必要あり
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
    }
}
// アプリ用のbuild.gradle
android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.woshidan.newtestapplication"
        minSdkVersion 15
        targetSdkVersion 26
        buildToolsVersion '26.0.1'
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary= true
    }
...
# プロジェクトのAndroidTest全体を行う(JUnitを使うUnitテストは connectedAndroid を取る)
$ ./gradlew app:connectedAndroidTest

# プロジェクトのAndroidTestのうち特定のメソッドのものを行う
$ ./gradlew app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.woshidan.myapplication.MainActivityTest#mainActivityTest
// 出力される実行結果
// build/generated/outputs/connected/以下にある
// build/generated/reports/以下にはHTML形式のレポートも
<?xml version='1.0' encoding='UTF-8' ?>
<testsuite name="com.example.woshidan.myapplication.MainActivityTest" tests="1" failures="0" errors="0" skipped="0" time="0.665" timestamp="2017-12-20T17:11:10" hostname="localhost">
  <properties>
    <property name="device" value="Nexus_5X_API_22(AVD) - 5.1.1" />
    <property name="flavor" value="" />
    <property name="project" value="app" />
  </properties>
  <testcase name="mainActivityTest" classname="com.example.woshidan.myapplication.MainActivityTest" time="0.563" />
</testsuite>

まとまってませんが色々試して満足したので現場からは以上です。

ライブラリ開発屋がAthenaを利用してログの収集分析をやりやすくした話

この記事はServerless Advent Calender 2017の16日目の記事です。

ライブラリ開発屋として仕事でAthenaを使ってログの収集分析をやりやすくした話をします。

はじめに

普段は開発者としてiOS/Android両対応のモバイルアプリ向けのライブラリの開発やテストをしています。その業務の中でお問い合わせを受けた際、お客さんの状況を聞いてライブラリを修正したり使い方を提案したりして対応させていただくことがあります*1

その中で、なかなか言葉で状況の説明が難しい場合があり、そういうときは動作検証時のログをいただいて状況の確認をさせていただきます。しかし、いかんせんそういう状況は再現が難しかったりするもので動作検証のログがとれないか試しているうちに時間が経ってしまってもどかしいことが結構ありました。

そこで、なるべく早くお客様に解決方法の提案ができるように、それらしいログを検索して取得できたらいいな、セキュリティ的・S3の予算的にピンポイントで取得したいな、ということで今回Athenaを使ってやってみました。

Athenaについて簡単に

Athenaは標準的なSQLを利用してS3内のデータを分析できるサービスです。

実際中で動いているのは分散SQLクエリエンジンのPrestoで、データフォーマットにJSON形式を用いる場合は下記のような形式のJSONが入っているS3のバケットのパスが s3://athena-examples/users/logs だったとして

{
  "name": "太郎",
   "address": "日本のそのあたり",
   "comment": "JSONは実際は1行にminifyしておく必要があります"
}

以下のクエリを実行すればSQLを用いて s3://athena-examples/users/logs 以下のデータを検索できるようにしてくれます。

// 単純化のため少々それっぽくないログとなっています
CREATE EXTERNAL TABLE IF NOT EXISTS user_logs (
  name string, 
  address string, 
  comment string
 ) 
ROW FORMAT  serde 'org.apache.hive.hcatalog.data.JsonSerDe'
LOCATION 's3://athena-examples/users/logs/';

テーブル定義の際に指定した LOCATION のパス以下はテーブル定義の形式に沿って処理できるデータのみが入っている必要がありますが、上記のテーブル定義はAthenaが検索の際に用いるだけで、S3に入っているデータとは結びついていないので何度でも破棄したり作り直すことが可能です。

他に対応しているデータフォーマットなど、詳しい話についてはこちらをご覧ください。

実際にやってみて気を使ったこと

やってみたことを書くとログの形式をAthenaで検索しやすい形に変更してAthenaを使いましたで終わってしまうので、その際気を使ったことなどを書いていこうかと思います。

Athenaのコスト対策の話

Athenaの料金形態は検索を実行するときのデータスキャン量に対する従量課金で、 スキャンされたデータ 1 TB あたり 5 USD となっています*2

そのため、ログをgzipなどに圧縮するとスキャン対象の容量をかなり削減することができます。幸いログファイルには同じ文字列が多く含まれるためか、自分が行った事前調査では単純にgzipにしただけで圧縮前の容量の15~20%まで小さくすることができました。S3にアップロードするファイルの圧縮は比較的簡単に対策ができるためやったほうがいいでしょう。

Athenaのコスト対策はAthenaの料金の話だけでは終わりません。AthenaはS3にあるファイルを取得するためにS3を利用しており、Athenaの料金とは別途にS3の利用料金がかかります。このS3の利用料金が場合によってはAthenaのスキャン料金と同等以上になることがあり、

  • 同じクエリを何度も実行しないようにする
  • LIMIT句をつけると早めにスキャン & データ取得を打ち切るので必ずLIMIT句をつけるようにする
  • パーティションやテーブル定義に利用するロケーションのパスを工夫してスキャン範囲を狭くする

などの対応を取る必要があります。

パーティションやテーブル定義に利用するロケーションのパスを決めるにあたって

前節で パーティションやテーブル定義に利用するロケーションのパスを工夫してスキャン範囲を狭くする と書きましたが、バケットのパスやパーティション分割のために使える要素としては、モバイルライブラリの場合

  • 日付
  • 対応OS
  • ライブラリのバージョン
  • ユーザのサービスアカウントごとのid

などがあります。それぞれ、data=xxx/os=ios/version=nnn/user_app_id=xxxx のように素直にパーティションを掘ってもよいかもしれませんが、

  • ライブラリのバージョンは企業によってはなかなか更新の時間がとれないため、利用されているバージョンが10種類以上とばらついてしまう
  • 利用されているサービスアカウントはテスト用のものもあるので1日に数百以上ある

ということを踏まえると、あっという間にAthenaが制限しているパーティション上限数 20000に引っかかってしまうでしょう。

しかし、日頃の調査を振り返ると

  • Android用ライブラリの調査をするときにiOS用ライブラリの調査をすることはほとんどない
  • ライブラリのログは利用している開発者のアプリごとの利用者規模が6桁~導入時のお試しで1桁、2桁まで様々
    • ライブラリのバージョンで区切って検索しているつもりが一番利用者数の多いアプリのログしかスキャンしていないことがありうる
  • ログを調査するのはお問い合わせ起点、つまり、調査対象のサービスアカウントや利用バージョンの情報を持っていることが多い
  • 新バージョンの不具合などにもとづいてログを調べるとしたら、結局は最新の日付から探していくことが多いだろう

などの事実があり、これらを踏まえると、同時に範囲検索で使う要素は存外少なそうなことがわかります。

結局、大きなテーブルを一つ定義してその中でいくつかのパーティションを利用するのではなく、基本的にはテーブルのロケーションに深めのパスを設定し、そのパスごとにたくさんのテーブルを定義しては捨てる方針にしました。

ややかっこわるいのですが、放っておくとパーティションのキーにあたる値の組み合わせは基本的にどんどん増えていくため、ほとんど一つのクエリで同時に検索しない範囲はテーブルのロケーションの方に突っ込んでもよいと思います。

スクリプトでクエリを生成することにしてクエリ実行結果の再利用やLIMIT句の指定を徹底する

Athenaは従量課金性ですが、一度実行したクエリの結果を再利用することが可能です。クエリを実行した時のquery execution idをパラメータにしてGetQueryResultsのAPIにリクエストを送ることにより、データのスキャンを再度行わずに実行結果を再取得することができます*3

なので、一度実行したクエリとそれに対応するquery execution idを控えて、実行しようとしているクエリが直近で実行されたものなら以前の結果を再利用することで、コストを抑えることができます。

これも結構簡単なスクリプトで対応を行うことが可能です*4

表記揺れによってほぼ同じクエリが別のクエリとカウントされて似たようなクエリが何個も走るかもしれませんが、いっそのことクエリの生成もスクリプトで行うことにしました。そうすることで、

  • 表記揺れがないため、クエリとquery execution idの対応を管理するスクリプトが動きやすい
  • テーブルのロケーションを深めに掘ったり、セッションの属性と紐づけてログを検索・分析しやすくした結果、肥大化したテーブル定義を間違えない
  • LIMIT句の追加などクエリの中で外してはいけないルールが徹底される

ことになりました。--dry-run オプションでAthenaに投げる予定のクエリを吐き出すようにしていて、手動でクエリを書きたくなった場合の下書きとしても利用できるようにしたり、実行時のログを一部出力することで、目当てのログのパスを見つけやすくしてなるべくターミナル一つで完結するように工夫しています。最近のブームは小規模なスクリプトDIYです*5

今後

今回の対応で社内外からお問い合わせがきたらログを分析しやすくなったので、今後はお問い合わせが来る前になにか気づけたらいいなということで、S3とLamdbaを組み合わせて不具合が起きてそうなときのログの検知もやってみたいと考えています。

DIYの現場からは以上です。

*1:サポートの方とお客様にひたすら助けられるお仕事とも言います

*2:https://aws.amazon.com/jp/athena/pricing/

*3:http://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryResults.html

*4:こういう http://woshidan.hatenablog.com/entry/2017/10/04/055335

*5:自分以外みんな知ってそうな余談であれなのですが、検索結果は1行1レコードとなっています。一回のクエリで同じファイルが複数レコードとして引っかかることがあるため、類似の用途で使う場合はLIMIT句の数字を用途に応じて増減しましょう

「テストも開発もするモバイルエンジニアのためのXCUITest/Espressoのすすめ」という題でLTをしました

testautomationresearch.connpass.com

要件の細かいことを突いたりテストケースの設計が大好物だったため、そういうのを本職としているテストエンジニアはどういう人たちなのかを知りたかったので「システムテスト自動化カンファレンス2017-2」に参加してきました。

図々しく「テストも開発もするモバイルエンジニアのためのXCUITest/Espressoのすすめ」という題でLTをさせていただいたので、取りいそぎスライドとその補足についてだけでもメモします。

スライド

speakerdeck.com

スライド補足

Appiumはいろんな言語のドライバがあるので好きな言語で書けるという話について

Appiumはブログなどの件数が多いから盛り上がっているという話がありますが、各言語のドライバでQ&Aやブログ記事が割れており、また言語を特定して情報を調べないとある言語の質問について他の言語で返ってくることもあるので*1、そういう数値については3~5分の1に割り引いて考えた方が良いかと思います。

各種言語のドライバの更新状況も言語によって異なっており、たとえば

  • Rubyの最新更新は約3週間前
  • perlの最新更新はREADME.mdで1年前
  • phpの最新更新は5か月前

となっています。不具合があった時に自分でAppiumが利用している各種ツールの状況を調査するコストが取れなければ、更新頻度の少ない言語は避け、いまならCookpad, Mercariが対応すると決めているRubyのドライバを選んだ方がハマりが少ないのではないでしょうか。。

Appiumが安定していなくてつらい話

Appiumは、

  • Appiumクライアント(Appiumクライアントライブラリ=各種言語のdriver, テストスクリプト)
  • Appiumサーバ
  • uiautomator/espresso or instrument/xcuitest

といった構成になっていて*2、テストを動かすために利用しているツールが多いです。

そのせいか、気軽にバージョンアップするとどれかのツールが噛み合わなくて不安定になったり動作が非常に遅くなったりします。

Execution on iOS is extremely slow after upgrading to Appium 1.6.5 · Issue #8717 · appium/appium · GitHub

Appium 1.7.1 Automation is too slow · Issue #739 · facebook/WebDriverAgent · GitHub

前述のドライバの言語がたくさんあることと合わせて関連issueがあちこちに散っており、かつ、大量のログが貼り付けられていることも多く必要な条件が見分けにくいため、調査に時間がかかります、というか、ました。

もともとSelenium系のツールに詳しいチームで対応をとったり、安定するまで待つ*3決定をすれば問題ないかもです。

しかし、現状のEspresso/XCUITestを直接使えば、間にそれなりに大きいツールをはさまないため、その分セットアップや調査が楽でした。

上記の「手間がかかる」「楽でした」という言葉について、調べている最中も悩んでいてこの補足を書いている現在も感じることですが、自分が不勉強で手間を惜しむなんて発想をするから云々みたいなことがずっと頭の中でくるくるしています。

ですが、テストも含めてコードは必要でなくなったら捨てたいです。気軽に捨てられるためには学習コストやメンテナンスコストも含めた手間は小さいほうがよいのでは、ということも一緒にぐるぐるしています。

LTタイトルに「モバイルエンジニア」を含めていますが、自分がアプリ開発者としての環境に慣れ親しんでいるのでEspresso/XCUITestで作ったテストなら気軽に捨てられるなぁ、という感じです。

E2Eテストが全部Espresso/XCUITestでいいかという話

もともと開発者*4の自分としてはAppiumで自動テストが書きやすい範囲とEspresso/XCUITestで自動テストが書きやすい範囲が被っているように感じました。それな らば、自動化しやすい部分は動かしやすいEspresso/XCUITestを使い、サーバとの連携、プッシュ通知であったりapk/ipaの更新時の挙動など自動化しにくい部分は手動テストと組み合わせればよい、と考えていました。

とくに、専用のQAチームを置けるほど規模が大きくないチームでE2Eテストが必要な時は「手動 + Appium」より「手動 + Espresso/XCUITest」の方が楽では~くらいの勢いでした。しかし、会場に行ってみると自分が想像していた以上にリリース用apk/ipaを用いたテストを重視する雰囲気だったため、やや場違いとなりもうしわけなかったです。

ただ、それぞれのプラットフォームのツールに乗っかって開発と同じIDEでテストを書くことにすると、テスト時の動作を入ってる変数付きで再現することができます。これにより、テストが悪いか/コードが悪いかの切り分けがかなり容易になり、開発しながらテストを書くハードルもぐっと下がるのでそれだけはお伝えしたかった(誰に。

遅くなった理由

もうちょっとややこしいレイアウトやもうちょっと新しいバージョンでもう一回Espresso Test Recorderの動作確認しようかと思ったらGradle Projectのリフレッシュがなかなか終わらなくてテンパってました。はい。。

最後に運営の皆様ありがとうございました。

現場からは以上です。

*1:たとえば http://discuss.appium.io/t/how-to-close-application-between-tests/1168 . phpの質問についてRubyJavaのコードがかえってきている

*2:https://github.com/appium/appiumhttp://www.atmarkit.co.jp/ait/articles/1504/27/news025.html を元にしています

*3:このことを考えると、更新が活発な言語を選択した方が良いと思います

*4:というか今もテスト設計の方が割いてる時間が多いだけで社内の肩書きとして開発者

xcodebuild testのテスト対象のデバイスやテストケースをappiumぽく簡単に指定できるラッパースクリプトを書いてみた

この記事はiOS2 Advent Calendar 2017の3日目の記事です。

xcodebuild testのテスト対象のデバイスやテストケースをややappiumみたいに簡単に指定できるラッパースクリプトを書いてみました。 なお、CocoaPodsでライブラリ管理をしているプロジェクトを対象としています。CocoaPodsをご利用でない場合は、適宜 -workspace-project オプションに読み替えていただけると幸いです。

スクリプト

スクリプト本体 xcode-wrapper.sh

#!/bin/bash

# configuration for project, written here
WORKSPACE="ExmapleApp.xcworkspace"
APP_SCHEME="ExmapleApp"
TEST_SCHEME="ExmapleAppTests"
BUILD_CONFIG="Debug"

# configuration for test target device written in caps.json
TARGET_LABEL=$(jq .target < ./caps.json)
DEVICE_NAME=$(jq .[$TARGET_LABEL]."deviceName" < ./caps.json | tr -d '"')
PLATFORM_VERSION=$(jq .[$TARGET_LABEL]."platformVersion" < ./caps.json | tr -d '"')
DEVICE_ID=$(jq .[$TARGET_LABEL]."udid" < ./caps.json | tr -d '"')
DEVICE_TYPE=$(jq .[$TARGET_LABEL]."type" < ./caps.json | tr -d '"')

build() {
  if [ "$DEVICE_TYPE" = "simulator" ]
  then
    # refs: https://qiita.com/roothybrid7/items/19f0aeeee98573dab2a6
    xcodebuild \
      -sdk iphonesimulator \
      -destination "platform=iOS Simulator,name=${DEVICE_NAME},OS=${PLATFORM_VERSION}" \
      -configuration "$BUILD_CONFIG" \
      -workspace "$WORKSPACE" -scheme "$APP_SCHEME" \
      build
  else
    xcodebuild \
      -sdk iphoneos \
      -destination "platform=iOS,id=${DEVICE_ID}" \
      -configuration "$BUILD_CONFIG" \
      -workspace "$WORKSPACE" -scheme "$APP_SCHEME" \
      build
  fi
}

test() {
  if [ "$DEVICE_TYPE" = "simulator" ]
  then
    xcodebuild -workspace $WORKSPACE \
      -scheme "$TEST_SCHEME" \
      -sdk iphonesimulator \
      "$TEST_CASE_OPTION" \
      -destination "platform=iOS Simulator,name=${DEVICE_NAME},OS=${PLATFORM_VERSION}" test
  else
    xcodebuild -workspace $WORKSPACE \
      -scheme "$TEST_SCHEME" \
      -sdk iphoneos \
      "$TEST_CASE_OPTION" \
      -destination "platform=iOS,id=${DEVICE_ID}" test
  fi
}

if [ $1 == "build" ]; then
    build
fi

if [ $1 == "test" ]; then
    i=1
    for OPT in "$@"
    do
        if [ $OPT = "-test" ]; then
            TEST_CASE_OPTION="-only-testing:${TEST_SCHEME}/${@:$(expr $i + 2):1}"
            test
            exit 0
        fi
        i=$(expr $i + 1)
    done
    test
fi

設定ファイル caps.json

"target" の部分を変更してテストを実行するデバイスを切り替えます。

{
  "target": "iPhone5", // テストで使うデバイスの指定
  "iPhone5": {
    "type": "simulator", // シミュレータ
    "deviceName": "iPhone 5",
    "platformVersion": "9.3",
    "udid": "" // シミュレータの指定では使わない
  },
  "iPhone6": {
    "type": "simulator", // シミュレータ
    "deviceName": "iPhone 6",
    "platformVersion": "10.0",
    "udid": "" // シミュレータの指定では使わない
  },
  "iPhone7": {
    "type": "simulator", // シミュレータ
    "deviceName": "iPhone 7",
    "platformVersion": "11.1",
    "udid": "" // シミュレータの指定では使わない
  },
   "iPad": {
    "type": "iphoneos", // 実機
    "deviceName": "woshidanのiPad", "" // 実機の指定では使わない
    "platformVersion": "10.3",
    "udid": "DUMMY_ID"
  }
}

使用例

# 全てのテストを実行する
$ ./xcode-wrapper.sh test

# ExmapleTestクラスにおいたテストをすべて実行する
$ ./xcode-wrapper.sh test -test ExmapleTest

# ExmapleTestクラスにおいたテストメソッドtestAを実行する
$ ./xcode-wrapper.sh test -test ExmapleTest/testA

# アプリをビルドする
$ ./xcode-wrapper.sh build

補足

実行されるテストを指定したい

CLIから見守るだけとはいえ、毎回全てのテストを実行するのは時間がかかります。 xcodebuild test サブコマンドは only-testing オプションを用いることで実行するテストを指定することができます。

https://stackoverflow.com/questions/39427263/how-to-use-xcodebuild-with-only-testing-and-skip-testing-flag

上記スクリプトでは、-test オプションを指定することで only-testing オプションで指定する文字列を与えることができます。スクリプト自体は -test オプションなしでも利用可能で、その場合は全てのテストを実行します。

利用できるデバイスの名前やID, OSバージョンの組み合わせについて

caps.json に書くシミュレータや実機デバイスの名前、OSバージョン、IDなどがわからない場合はXCodeAdd Simulators ... の画面か $ instruments -s devices コマンドより確認できます。

XCodeのデバイス管理画面から

f:id:woshidan:20171202232044p:plain

ターミナルから

$ instruments -s devices
...
iPhone SE (10.0) [7DB19BC4-5A56-4D40-93BD-90F551713043] (Simulator)
iPhone SE (10.3.1) [10FE1E72-D129-4593-A18B-AE85F4D9DC78] (Simulator)
iPhone SE (11.1) [C511B853-09C3-4599-A88F-737296F02DED] (Simulator)
iPhone X (11.1) [95022FBF-DDDB-46A8-8978-0129C0A6142D] (Simulator)
...

もっと色々書こうかと思ったのですが力尽きたので現場からは以上です。

XCode Tools CLIのバージョンアップとビルドに必要なパラメータ、権限の変化について

お仕事の都合で古いOSに対してもXCode Tools CLIを使ってビルドする必要があって調べてみても要領を得なかったんだけど少しわかった気がするので、わかったところまで整理してみます*1

アプリとして実機やシミュレータにインストールできるファイルの種類について

説明
.app xcrun simctl を経由して iPhone simulatorへインストールできる。 xcodebuild build コマンドやXCodeでシミュレータへ向けてビルドすることで作成可能
.ipa アプリストアへリリースしたり、実機へデプロイするときに作成する必要があるファイル形式。 .app.xcarchive ファイルから開発者の証明書を何らかの形でアタッチするようなプロセスを経て作成

参考

XCode7時代の .ipa 作成方法とXCode8時代の .ipa 作成方法

  • XCodeが7の頃は .ipa 作成に関して .app ファイルを渡せば xcrunPackageApplication がうまくやってくれていた
  • XCode8.3. で PackageApplication が廃止されてしまった
    • XCode8.3.2 を使う場合 xcrun: error: unable to find utility "PackageApplication", not a developer tool or in PATH というメッセージが出るようになっている
  • このため、 xcrun PackageApplication の代わりに xcodebuild archive を使う必要がある
    • xcodebuild.xcarchive を作成した後、 .ipa を作成する際に exportProvisioningProfile などを自分で設定する必要がある
    • これの具体的なパス (SampleXXX的なノリなのではなく 'MyMobileApp Distribution Profile' みたいなのりでファイル名などが想像しにくい例が多い) がよくわからなくてつらい

参考

XCode 8.2 -> XCode 8.3 で Automatically manage signing が有効になっているプロジェクトにおいて .ipa へのエクスポートが失敗するように

  • [試せていない] exportOptions.plistmethod の値が development でない場合、XCodeでビルドしているときに、ログインしているアカウントが Admin でない場合、、証明書(開発者登録. キーチェーンに登録している) と Provisioning Profile (開発者とappIdに紐づいている) が揃っていても権限がたりなくて export に失敗する
    • [試せていない] CI用にadminにする?
  • [試せていない] xcodebuild -exportArchive -exportOptionsPlist /PATH/exportOptions.plist で指定する exportOptions.plistmethod の値を development にすればうまくいくかも?
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>development</string>
    <key>teamID</key>
    <string>GLPGBQ39X8</string>
</dict>
</plist>

参考

どうすればいいかわかったら今後追記しますが、詳しい人教えて欲しい感じです。

*1:上長許可あり

ERBとCSVを使って簡単な自分用ドリルを作る

最近シェルの勉強をしたいんですが、以前SQLを勉強した時のように基本的なシェルを反復して書くのが一番では? と思って、ドリルのようなものが欲しくなりました。

そこで、CSVにお題とお題の答えを書き連ねたらアコーディオンで答えを出したり隠したりしてくれるHTMLを出すコードと、そのHTMLを見るアプリ*1を書いたのでポスト。

github.com

使い方は、

$ ruby generator/generator.rb input.csv > drill.html

を実行して drill.html をブラウザで開く、です。アプリで見る場合はさらに

$ cp drill.html cp drill.html CSDriller/app/assets/drill.html

とした後、一緒についているAndroidプロジェクトをビルドしてください。

実際動かしてみると下記のような感じです。

参考

qiita.com

特に上記記事のアコーディオンがよすぎて作りたくなったので多謝。

*1:といってもAndroidのWebViewに突っ込むだけ