cypress + nyc + next.js + CircleCI + codecov での環境構築
TL;DR
cypressとnyc, CircleCI, codecovでの環境構築を行うステップと、CircleCIのconfig.ymlの共有と解説を行います。 これでe2eテストをCIで実行し、テストカバレッジをプルリク上で確認できより高品質なコードを保てるようになるかと思います。
前提
すでにCypressをインストール、またはcypress、ncy、CircleCI、codecovとは何かについて知識のある方を前提としています。
Cypressでカバレッジ設定
Cypressにてカバレッジを吐き出すように公式サイトを参考に設定を行います。
1. 必要なパッケージのインストール
yarn add -D babel-plugin-istanbul @cypress/code-coverage nyc istanbul-lib-coverage
2..babelrc
の編集
.babelrc
の plugin
に istanbul
を追加します。
{ "presets": ["@babel/preset-react"], "plugins": ["transform-class-properties", "istanbul"] }
3.cypress/support/index.js
の編集
cypress/support/index.js
に import '@cypress/code-coverage/support'
を追加します
// cypress/support/index.js import '@cypress/code-coverage/support'
4.cypress/plugins/index.js
の編集
on('task', require('@cypress/code-coverage/task'))
を cypress/plugins/index.js
の modue.exports
内に追加します。
// cypress/plugins/index.js module.exports = (on, config) => { on('task', require('@cypress/code-coverage/task')) }
これで coverageが /coverage
に吐かれるようになったかと思います。
CircleCI + codecov設定
circleci/config.yml
に下記ファイルを設定します。
config.yml
version: 2.1 orbs: cypress: cypress-io/cypress@1 jobs: codecov: executor: cypress/base-10 steps: - attach_workspace: at: ~/ - run: yarn install - run: yarn codecov workflows: build: jobs: - cypress/run: yarn: true start: yarn next wait-on: 'http://localhost:3000' post-steps: - store_artifacts: path: coverage/ - codecov: requires: - cypress/run
解説
まずcypressを動かすために orbs
で提供されている cypress-io/cypress@1
を使用します。
version: 2.1 orbs: cypress: cypress-io/cypress@1
また cypress-io/cypress
にある cypress/run
job を設定します。
- cypress/run: yarn: true start: yarn next wait-on: 'http://localhost:3000' post-steps: - store_artifacts: path: coverage/
yarn: true
: yarnを使用しているため、npmであれば不要start
: cypress runが事項される前にlocalhost:3000
へとアクセスできるようにするためyarn next
を指定。(next 以外ではここを変更する)wait-on
: 上記のコマンドでlocalhost:3000
へアクセスできるまで待機post-steps
: テスト実行後のステップを記入store_artifacts
: cypressにて吐き出されたreportを他jobで共有できるようにartifactを登録
そして上記ステップ完了後にcodecovにreportを送信します。 もしcodecovへ登録が行われていない場合は公式サイトを参考に対応を行ってください。
codecovのjobを登録
jobs: codecov: executor: cypress/base-10 steps: - attach_workspace: at: ~/ - run: yarn install - run: yarn codecov
(yarn installしていますが、orbsでキャッシュしたものを使用したほうが良いかもしれません)
- run: yarn codecov
でcodecovに orbs
にて作成したレポートを送信しています。
これで cypress + nyc + next.js + CircleCI + codecov での環境構築は完了です。
まとめ
意外と cypress + nyc + next.js + CircleCI + codecov
での環境について情報がまとまっていなかったのでブログに書きました。
codecovはテストコードをチームで書く上でかなり便利なサービスかと思います。
ぜひこの機会に高品質なコードを保守できる環境を整えてみてはいかがでしょうか。
HerokuでIP制限
HerokuにてRailsアプリケーションを公開したは良いのですが、海外からのアクセスが多すぎてdynoが耐えきれず response timeout が大量発生してしまいました。 そこで今回はdynoに負荷をかけずに海外からのアクセス制限をした方法について書きます。
はじめに
HerokuではWebサーバーなどの設定が細かくできないため、プロジェクト内に設定ファイルを書いてIP制限を行うということができません。 またRailsアプリで
などのgemを使うことで、IP制限を行うことができるのですがdynoへの負荷やアプリのパフォーマンス低下(上記だとRack::Attack.callが大量に呼ばれる)へと繋がるため、あまり良い選択ではないと思いました。 理想は許可したIPのみdynoへアクセスさせたいので、ネットワークでうまくできないかと調べたところCloudFlareを使えばうまくできそうだということがわかりました。
以下CloudFlareでの設定方法を説明します
CloudFlare
まずはCloudFlareで会員登録
次に登録したいサイトを入力します
60秒ほど待つとCNAMEのvalueが解析されるのでContinueを押します
そしてCloudFlareのネームサーバーの値が表示されるので、これを使用しているドメインに設定します。
ちなみにお名前ドットコムでは下記を編集します。
だいたい登録して1,2hで反映されましたが長いときには1日かかるときもあるそうです。
CloudFlareでのIP制限
CloudFlareでIP制限を行うためにはFirewallの設定を行います。 CloudFlare Firewallでは
- Cookie
- AS Num
- Country
- Host Name
- IP Adress
- Referer
- Request Method etc
などのフィールドを元に制限ができ非常に便利です。 今回は日本以外のアクセスを制限したかったため下記のように書きました。
普通国外を制限する場合は日本のIPアドレスすべて書き込む必要があるのですが、CloudFlareではたったこれだけで制限ができるので非常に便利ですね。
また国外にてどう表示されているかを確認する際は下記のchrome拡張を使うとスムーズに確認できます。
実際にサイトを見てみると
正常に制限されていることがわかりますね。
CloudFlareは今までCDNなどに用いるイメージがありましたが、無料で且つ簡単にIP制限ができるなんて素晴らしいサービスですね!
現場で使える Ruby on Rails 5速習実践ガイド(特典PDF付き)
- 作者: 大場寧子,松本拓也,櫻井達生,小田井優,大塚隆弘,依光奏江,銭神裕宜,小芝美由紀
- 出版社/メーカー: マイナビ出版
- 発売日: 2018/10/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
Ruby on Rails 5アプリケーションプログラミング
- 作者: 山田祥寛
- 出版社/メーカー: 技術評論社
- 発売日: 2017/04/14
- メディア: 大型本
- この商品を含むブログを見る
プロフェッショナルのための 実践Heroku入門 プラットフォーム・クラウドを活用したアプリケーション開発と運用 (書籍)
- 作者: 相澤歩,arton,鳥井雪,織田敬子
- 出版社/メーカー: KADOKAWA/アスキー・メディアワークス
- 発売日: 2014/09/19
- メディア: 大型本
- この商品を含むブログ (3件) を見る
Migrate ActiveAndroid to Room
以前作成したAndroidアプリのORMとしてActiveAndroidを使用していたのですが、Android 8.0からreceiver上でactive android経由でデータベースにアクセスすると下記のようなエラーが発生するようになりました。
java.lang.RuntimeException: at android.app.ActivityThread.handleReceiver (ActivityThread.java:3652) at android.app.ActivityThread.-wrap18 (Unknown Source) at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1985) at android.os.Handler.dispatchMessage (Handler.java:109) at android.os.Looper.loop (Looper.java:166) at android.app.ActivityThread.main (ActivityThread.java:7377) at java.lang.reflect.Method.invoke (Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:469) at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:963)
ActiveAndroidのrepositoryを見る限り、ActiveAndroidの開発自体も止まってるし、この機にRoomへ移行してみました。 ActiveAndroidからRoomへの移行は公式ホームページ通り進めていくと、いくつかつまづくポイントが出てきたので、そちらについてまとめたいと思います。
つまづきポイント
1: Dbにアクセスできない
公式ホームページではDbへのアクセスのために
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();
とデータベースの名前に拡張子がありませんが、ActiveAndroidからの移行の場合 database-name.db
と拡張子をつけないとアクセスできません。
2: マイグレーションが必要
マイグレーションの追加
Migrationをしない状態でアプリを起動すると下記エラーが発生します。
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
このエラーが発生した場合、マイグレーションの設定を記述する必要があります。
Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(object: Migration(activeAndroidMigrationVersion, activeAndroidMigrationVersion + 1){ override fun migrate(database: SupportSQLiteDatabase) { } }) .build();
上記のように設定するんですが、 activeAndroidMigrationVersion
にはActiveAndroidのmigration バージョンを指定してください。
整合性が取れたマイグレーションを記述
RoomではEntityで記述された内容がDBにそのまま書き込まれます。 この時ActiveAndroidで作成したテーブルとRoomで作成されるテーブルの整合性が取れない場合下記エラーが発生します。
Caused by: java.lang.IllegalStateException: Migration didn't properly handle
ActiveAndroidとRoomのテーブルが異なる原因として下記が考えられます。
- NOT NULL 成約の有無
Roomではfieldの型として int
や boolean
などのプリミティブ型を指定するとデフォルトでNOT NULLになるのですが、ActiveAndroidではこの制約がない場合が多い(アノテーションを加えなければいけない)ので、確認が必要です。
- カラム名が異なる
Roomではメンバ変数の名前がそのまま、tableのカラム名になります。もし、変数名と異なる名前をつけたい場合は
@ColumnInfo(name = "first_name")
のようにカラム名を直接していしてください。
また上記の不整合メッセージはそのままテキストで吐き出されるため、カラム数が多いとどのカラムが違うのか確認するのが大変です。 そこで比較が容易になるようスクリプトを作成しました。(Macでのみ動作します) 是非使ってみてください。
#!/bin/bash # Clean up on exit function finish { rm -f expected found } trap finish EXIT # How to parse JSON JQ="jq --sort-keys" # Convert log to JSON format toJson () { # First sed removes spaces between indices ("columns:[id, col1, col2]" becomes "columns:[id,col1,col2]") # Indeed, spaces are later replaced to quotes, and we don't want that (would cause parse errors: columns:[id,"col1,"col2]) # Second sed ensures single column indices have quotes sed -E ':a;s#(\[[[:alnum:],]*) ([[:alnum:] ,]*\])#"\1\2"#;ta' \ | sed -E 's/(\[[[:alnum:]]+\])/"\1"/g' \ | sed -e 's/} ,/},/g' \ -e 's/Column//g' \ -e 's/Index//g' \ -e "s/'/\"/g" -e 's/=/":/g' -e 's/ /"/g' -e 's/{/{"/g' \ | sed -e 's/TableInfo//g' } # Check arg in="$1" if [ "$in" == "" ] then echo "Usage: $0 <dump of error message>" >&2 echo "For example:" >&2 echo "Usage: $0 \"java.lang.IllegalStateException: Migration failed. expected:TableInfo{name='...', columns={...}, foreignKeys=[], indices=[]} , found:TableInfo{name='...', columns={...}, foreignKeys=[], indices=[]}\"" >&2 exit 1 fi # Parse input: split into expected/found splitInput=$(cat "$in" | sed -e $'s/expected:TableInfo/\\\n/g' -e $'s/ , found:TableInfo/\\\n/g') echo "$splitInput" # Diff 'expected' and 'found' as JSON echo "$splitInput" | head -n 3 | tail -n 1 | toJson | $JQ . >expected echo "$splitInput" | tail -1 | toJson | $JQ . >found vimdiff expected found
使用方法
- 上記スクリプトを
diff.sh
という名前で保存。 jq
を使うので、なければbrew install jq
コマンドを打ってインストール。- エラーログを
error.txt
というファイルに保存。 bash diff.sh error.txt
とコマンドを打つ。
上記の手順を踏めば、ActiveAndroidテーブルとRoomテーブルの差分が表示されるので、容易に修正することができます。 またAndroidではSqliteを使用しているので、カラムの成約変更ができないため、tmpテーブルを作成して移行してください。
【Android】 getApplication() をCustomApplicationにCastできる仕組み
Android開発をしたことがある方は、Activity, FragmentなどでgetApplication
を使いApplicationを呼び出しCustomApplicationにCastしたkことがあるのではないかと思います。
そのときなんでApplicationをCustomApplicationにCastしたときjava.lang.ClassCastException
が発生しないんだろう?どういう仕組なんだろう?と疑問に感じたことは無いでしょうか?
今回はその仕組について説明したいと思います。
疑問
そもそもCustomApplicationは
public class CustomApplication extends Application { @Override public final void onCreate() { super.onCreate(); } }
の様にApplicationのサブクラスになるため、直接生成したApplicationをCustomApplicationにキャストしようとすると java.lang.ClassCastException
が発生するはずです。
また Application
は
public class Application extends ContextWrapper implements ComponentCallbacks2 { .... }
public class ContextWrapper extends Context { ... }
となっているため、同様に直接生成したContextをCustomApplicationにCastするとエラーが発生するはずです。
薄々気づいてるかもしれませんが、ダウンキャスト時にエラーが発生しないということは内部でCustomApplication->Application->CustomApplication又はCustomApplication->Context->CustomApplicationの順でキャストがなされているはずです。
どのように行われているか見ていきます。
仕組み
Activity内でgetApplicationをした時
Activity.java
/** Return the application that owns this activity. */ public final Application getApplication() { return mApplication; }
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this, window); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } if (info.uiOptions != 0) { mWindow.setUiOptions(info.uiOptions); } mUiThread = Thread.currentThread(); mMainThread = aThread; mInstrumentation = instr; mToken = token; mIdent = ident; mApplication = application; mIntent = intent; mReferrer = referrer; mComponent = intent.getComponent(); mActivityInfo = info; mTitle = title; mParent = parent; mEmbeddedID = id; mLastNonConfigurationInstances = lastNonConfigurationInstances; if (voiceInteractor != null) { if (lastNonConfigurationInstances != null) { mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor; } else { mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this, Looper.myLooper()); } } mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); if (mParent != null) { mWindow.setContainer(mParent.getWindow()); } mWindowManager = mWindow.getWindowManager(); mCurrentConfig = config; }
全体コード(http://tools.oesf.biz/android-7.1.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java)
Activityを継承したクラスでgetApplicationをするとActivity内のメンバ変数mApplication
が返されていることがわかります。
またmApplication
はattach
メソッドが呼び出された時に初期化されています。
またActivity.attach
はActivityThread.performLaunchActivity
で呼び出されており
ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { 2518 // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")"); 2519 2520 ActivityInfo aInfo = r.activityInfo; 2521 if (r.packageInfo == null) { 2522 r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, 2523 Context.CONTEXT_INCLUDE_CODE); 2524 } 2525 2526 ComponentName component = r.intent.getComponent(); 2527 if (component == null) { 2528 component = r.intent.resolveActivity( 2529 mInitialApplication.getPackageManager()); 2530 r.intent.setComponent(component); 2531 } 2532 2533 if (r.activityInfo.targetActivity != null) { 2534 component = new ComponentName(r.activityInfo.packageName, 2535 r.activityInfo.targetActivity); 2536 } 2537 2538 Activity activity = null; 2539 try { 2540 java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); 2541 activity = mInstrumentation.newActivity( 2542 cl, component.getClassName(), r.intent); 2543 StrictMode.incrementExpectedActivityCount(activity.getClass()); 2544 r.intent.setExtrasClassLoader(cl); 2545 r.intent.prepareToEnterProcess(); 2546 if (r.state != null) { 2547 r.state.setClassLoader(cl); 2548 } 2549 } catch (Exception e) { 2550 if (!mInstrumentation.onException(activity, e)) { 2551 throw new RuntimeException( 2552 "Unable to instantiate activity " + component 2553 + ": " + e.toString(), e); 2554 } 2555 } 2556 2557 try { 2558 Application app = r.packageInfo.makeApplication(false, mInstrumentation); 2559 2560 if (localLOGV) Slog.v(TAG, "Performing launch of " + r); 2561 if (localLOGV) Slog.v( 2562 TAG, r + ": app=" + app 2563 + ", appName=" + app.getPackageName() 2564 + ", pkg=" + r.packageInfo.getPackageName() 2565 + ", comp=" + r.intent.getComponent().toShortString() 2566 + ", dir=" + r.packageInfo.getAppDir()); 2567 2568 if (activity != null) { 2569 Context appContext = createBaseContextForActivity(r, activity); 2570 CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); 2571 Configuration config = new Configuration(mCompatConfiguration); 2572 if (r.overrideConfig != null) { 2573 config.updateFrom(r.overrideConfig); 2574 } 2575 if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity " 2576 + r.activityInfo.name + " with config " + config); 2577 Window window = null; 2578 if (r.mPendingRemoveWindow != null && r.mPreserveWindow) { 2579 window = r.mPendingRemoveWindow; 2580 r.mPendingRemoveWindow = null; 2581 r.mPendingRemoveWindowManager = null; 2582 } 2583 activity.attach(appContext, this, getInstrumentation(), r.token, 2584 r.ident, app, r.intent, r.activityInfo, title, r.parent, 2585 r.embeddedID, r.lastNonConfigurationInstances, config, 2586 r.referrer, r.voiceInteractor, window); 2587 2588 if (customIntent != null) { 2589 activity.mIntent = customIntent; 2590 } 2591 r.lastNonConfigurationInstances = null; 2592 activity.mStartedActivity = false; 2593 int theme = r.activityInfo.getThemeResource(); 2594 if (theme != 0) { 2595 activity.setTheme(theme); 2596 } 2597 2598 activity.mCalled = false; 2599 if (r.isPersistable()) { 2600 mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); 2601 } else { 2602 mInstrumentation.callActivityOnCreate(activity, r.state); 2603 } 2604 if (!activity.mCalled) { 2605 throw new SuperNotCalledException( 2606 "Activity " + r.intent.getComponent().toShortString() + 2607 " did not call through to super.onCreate()"); 2608 } 2609 r.activity = activity; 2610 r.stopped = true; 2611 if (!r.activity.mFinished) { 2612 activity.performStart(); 2613 r.stopped = false; 2614 } 2615 if (!r.activity.mFinished) { 2616 if (r.isPersistable()) { 2617 if (r.state != null || r.persistentState != null) { 2618 mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, 2619 r.persistentState); 2620 } 2621 } else if (r.state != null) { 2622 mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); 2623 } 2624 } 2625 if (!r.activity.mFinished) { 2626 activity.mCalled = false; 2627 if (r.isPersistable()) { 2628 mInstrumentation.callActivityOnPostCreate(activity, r.state, 2629 r.persistentState); 2630 } else { 2631 mInstrumentation.callActivityOnPostCreate(activity, r.state); 2632 } 2633 if (!activity.mCalled) { 2634 throw new SuperNotCalledException( 2635 "Activity " + r.intent.getComponent().toShortString() + 2636 " did not call through to super.onPostCreate()"); 2637 } 2638 } 2639 } 2640 r.paused = true; 2641 2642 mActivities.put(r.token, r); 2643 2644 } catch (SuperNotCalledException e) { 2645 throw e; 2646 2647 } catch (Exception e) { 2648 if (!mInstrumentation.onException(activity, e)) { 2649 throw new RuntimeException( 2650 "Unable to start activity " + component 2651 + ": " + e.toString(), e); 2652 } 2653 } 2654 2655 return activity; 2656 }
2558 Application app = r.packageInfo.makeApplication(false, mInstrumentation);
2583 activity.attach(appContext, this, getInstrumentation(), r.token, 2584 r.ident, app, r.intent, r.activityInfo, title, r.parent, 2585 r.embeddedID, r.lastNonConfigurationInstances, config, 2586 r.referrer, r.voiceInteractor, window);
2588行目で作成したApplication型のインスタンスをactivity.attach
の引数で渡していることがわかります。
この時makeApplication
の中身が気になるので調べてみると
LoadedApk.java
772 public Application makeApplication(boolean forceDefaultAppClass, 773 Instrumentation instrumentation) { 774 if (mApplication != null) { 775 return mApplication; 776 } 777 778 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication"); 779 780 Application app = null; 781 782 String appClass = mApplicationInfo.className; 783 if (forceDefaultAppClass || (appClass == null)) { 784 appClass = "android.app.Application"; 785 } 786 787 try { 788 java.lang.ClassLoader cl = getClassLoader(); 789 if (!mPackageName.equals("android")) { 790 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, 791 "initializeJavaContextClassLoader"); 792 initializeJavaContextClassLoader(); 793 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); 794 } 795 ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); 796 app = mActivityThread.mInstrumentation.newApplication( 797 cl, appClass, appContext); 798 appContext.setOuterContext(app); 799 } catch (Exception e) { 800 if (!mActivityThread.mInstrumentation.onException(app, e)) { 801 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); 802 throw new RuntimeException( 803 "Unable to instantiate application " + appClass 804 + ": " + e.toString(), e); 805 } 806 } 807 mActivityThread.mAllApplications.add(app); 808 mApplication = app; 809 810 if (instrumentation != null) { 811 try { 812 instrumentation.callApplicationOnCreate(app); 813 } catch (Exception e) { 814 if (!instrumentation.onException(app, e)) { 815 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); 816 throw new RuntimeException( 817 "Unable to create application " + app.getClass().getName() 818 + ": " + e.toString(), e); 819 } 820 } 821 } 822 823 // Rewrite the R 'constants' for all library apks. 824 SparseArray<String> packageIdentifiers = getAssets(mActivityThread) 825 .getAssignedPackageIdentifiers(); 826 final int N = packageIdentifiers.size(); 827 for (int i = 0; i < N; i++) { 828 final int id = packageIdentifiers.keyAt(i); 829 if (id == 0x01 || id == 0x7f) { 830 continue; 831 } 832 833 rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id); 834 } 835 836 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); 837 838 return app; 839 }
全体コード(http://tools.oesf.biz/android-7.1.0_r1.0/xref/frameworks/base/core/java/android/app/LoadedApk.java)
796 app = mActivityThread.mInstrumentation.newApplication( 797 cl, appClass, appContext);
となっておりInstrument.newApplication
からApplication
を作成していることがわかります。
また引数として渡しているappClass
は
780 Application app = null; 781 782 String appClass = mApplicationInfo.className; 783 if (forceDefaultAppClass || (appClass == null)) { 784 appClass = "android.app.Application"; 785 }
となっており、forceDefaultAppClass
(makeApplicationの引数)がtrue
又はmApplicationInfo.className
がnull
の時にandroid.app.Application
が代入されるのがわかります。
ちなみにこの時のmApplicationInfo.classNameにはAndroidManifest.xmlにかかれている
<application android:name=".CustomApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
のandroid:name
に指定された値が入ってきます。
989 public Application newApplication(ClassLoader cl, String className, Context context) 990 throws InstantiationException, IllegalAccessException, 991 ClassNotFoundException { 992 return newApplication(cl.loadClass(className), context); 993 } 994 1004 static public Application newApplication(Class<?> clazz, Context context) 1005 throws InstantiationException, IllegalAccessException, 1006 ClassNotFoundException { 1007 Application app = (Application)clazz.newInstance(); 1008 app.attach(context); 1009 return app; 1010 }
1007 Application app = (Application)clazz.newInstance();
遂にたどり着きました。ココでappClass
を元にCustomApplicationを作成し、それをApplicationにダウンキャストしていることがわかります。
このようにしてgetApplication()で取得するApplicationはCustomApplicationにキャストできるわけですね。
ViewPagerの入れ子構造でFragmentが表示されない時の対処
ViewPagerを使ってフラグメントを表示するときに普通 FragmentPagerAdapter
を継承したカスタムのAdapterにFragmentManagerを引数として渡すと思いますが、ViewPagerが入れ子構造をしている場合、子のフラグメントのViewが表示されない問題が発生してしまいます。
この問題の対処として
CustomAdapter adapter = CustomAdapter(getActivity().getSupportFragmentManager())
を
CustomAdapter adapter = CustomAdapter(getChildFragmentManager())
のように書き換えてください。
getChildFragmentManager()
の中身がどのようになっているか見てみると
final public FragmentManager getChildFragmentManager() { if (mChildFragmentManager == null) { instantiateChildFragmentManager(); if (mState >= RESUMED) { mChildFragmentManager.dispatchResume(); } else if (mState >= STARTED) { mChildFragmentManager.dispatchStart(); } else if (mState >= ACTIVITY_CREATED) { mChildFragmentManager.dispatchActivityCreated(); } else if (mState >= CREATED) { mChildFragmentManager.dispatchCreate(); } } return mChildFragmentManager; }
とActivityのステイタスによってmChildFragmentManagerにライフサイクルに対応したメソッドを呼び出しています。
getActivity().getSupportFragmentManager()
を子のフラグメントで直接呼び出すとライフサイクルを無視し、バックグラウンドに存在するフラグメントが表示されないというバグが発生してしまったことがわかります。
Acer Liquid Z200 Android 4.4 / AndroidデュアルSIM&SIMロックフリー / 4inch ディスプレイ / RAM 512MB / ROM 4GB
- 出版社/メーカー: Acer
- メディア: Wireless Phone Accessory
- この商品を含むブログ (1件) を見る
ほんきで学ぶAndroidアプリ開発入門 第2版 Android Studio、Android SDK 7対応
- 作者: 株式会社Re:Kayo-System
- 出版社/メーカー: 翔泳社
- 発売日: 2016/11/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
Combining Textual Entailment and Argumentation Theory for Supporting Online Debates Interactions の概要と考察
今回は2012年に自然言語のトップカンファレンスであるACLに投稿されたCombining Textual Entailment and Argumentation Theory for Supporting Online Debates Interactions
についての概要を説明し、考察してみようと思います。
ACL | Association for Computational Linguistics
[原文]http://www.aclweb.org/anthology/P12-2041
序章
近年、TwitterやDebetepediaといったサービスで様々な話題で多くの議論が交わせれている。
しかし、そのような話題に途中で参加するには今までの議論を一つ一つ見ていき、議論が収束しているのか、また今後話し合う必要があるところなどを考えなくてはいけなく、それを面倒に感じることが多いのではないかと考えている。
そこでこの論文ではこれらのサービスで繰り広げられている議論をTextual Entailment
を使って、反対・賛成意見を抜き取り収束したのかどうかを評価する手法を提案している。
手法
Textual Entailment
とはDaganが2009年に提唱したもので、text(t)とhypothesis(h)で論理的含意(t→h)を表すものである
ちょっと何を言っているのかわからないので簡単な例を見てみると
text:
アメリカンショートヘアーを飼っている
hypothesis:
猫を飼っている
がある時textが真ならばhypothesisも真になるのがわかると思う。
このような関係がTextual Entailmentである。
数学的にこのような条件を定義する時は厳密なものでなくてはならないが、Textual Entailmentではすごくざっくりとしたものであるため、客観的に正しいと考えられるものはこのように定義できる。
ちなみに、TE(日本語)の評価データについては京大の黒橋・河原研究室のホームページに掲載されています。
Textual Entailment 評価データ - KUROHASHI-KAWAHARA LAB
この論文でも幾つかTextual Entailment の例を出しているので見てみると下記のように書いてある。
text:
Research shows that drivers speaking on a mobile phone have much slower reactions in braking tests than non-users, and are worse even than if they have been drinking.
hypothesis:
The use of cell-phones while driving is a public hazard.
この例文を意訳してみるとtextは「電話で話しながら運転をしていると、電話をしていない人々また飲酒運転をしている人よりもブレーキテストの反応が悪かった」と読め、hypothesisには「運転中の携帯電話の使用は危険である」と書かれており確かにこの場合でもtextが真ならばhypothesisが真であることが分かる。
このようにT-Hのペアを見つけることで、議論の関係性を見ることができ、また相手の発言に対して反論するときにTに対してなのかHに対してなのかを区別し最終的にどこで議論が収束していたのか見ている。
A1=hypothesis:
The use of cell-phones while driving is a public hazard.
A2=text:
Research shows that drivers speaking on a mobile phone have much slower reactions in braking tests than non-users, and are worse even than if they have been drinking.
A3=text:
Regulation could negate the safety benefits of having a phone in the car. When you’re stuck in traffic, calling to say you’ll be late can reduce stress and make you less inclined to drive aggressively to make up lost time.
A4=text:
If one is late, there is little difference in apologizing while in their car over a cell phone and apologizing in front of their boss at the office. So, they should have the restraint to drive at the speed limit, arriving late, and being willing to apologize then; an apologetic cell phone call in a car to a boss shouldn’t be the cause of one being able to then relax, slow-down, and drive the speed-limit.
これらの関係を図で表すと上記のようになり。点線の矢印は反論をそれ以外の矢印は賛成となる関係をしめしている。
また、これは実際にDebetepediaで繰り広げられた議論である、二重線で書かれている円の主張が受け入れられていることを表す。
このような関係を取り出すために著者はEDITS system
というオープンソースのTE認識システムを使って既存の手法と提案手法の実験を行い、評価を行った。
データセットには下記のような100個のtarin data, test dataを用いている。
結果を見てみると既存の手法ではtraining setでaccuracyが0.69,test setで0.67であったが提案手法では0.75と高くなった。
考察
議論の賛成、反対などの意見をTEに着目して分析したのは素晴らしい考えだと思う。
しかし、この論文では詳しい分類の実装方法などが書かれておらず実際に自分の手で実験できないのは残念である。
またACLといったトップカンファレンスはこういった比較的新しい手法に対してAcceptが寛容的になるのではないかと感じた。(それだけTEは今後流行ると期待されているのか)
今後もAugmentation関連の論文を読んで知見を集めていこうと思う。
- 作者: Steven Bird,Ewan Klein,Edward Loper,萩原正人,中山敬広,水野貴明
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/11/11
- メディア: 大型本
- 購入: 20人 クリック: 639回
- この商品を含むブログ (44件) を見る
- 作者: 高村大也,奥村学
- 出版社/メーカー: コロナ社
- 発売日: 2010/07
- メディア: 単行本
- 購入: 13人 クリック: 235回
- この商品を含むブログ (42件) を見る
- 作者: 奥村学
- 出版社/メーカー: コロナ社
- 発売日: 2010/10/15
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 379回
- この商品を含むブログ (11件) を見る
Predicting Quality Flaws in User-generated Content: The Case of Wikipedia の概要と考察
2012年にACMというカンファレンスに出された「Predicting Quality Flaws in User-generated Content: The Case of Wikipedia」という論文を読み考察をしました。
[原文] http://www.uni-weimar.de/medien/webis/publications/papers/stein_2012i.pdf
この論文ではWikipediaについているcleanup tagをもとに記事の質の悪さを予測する手法を提案している。
序章
現在誰もが知っているWikipediaには日々膨大な記事が作成されていおり、その著者に誰もがなれるため様々な質の記事が生み出されている。
本来は記事がリリースされる前に専門家のチェックを理想とするが、記事の量が膨大なため人手で目を通すのは不可能に近い。
そこで、この論文では記事の良し悪しを見るためcleanup tagを用いて、コンピューターが記事の欠落を予測する手法を提案している。
実験
cleanup tagとはWikipediaの記事の欠落を表すタグである。
下記の画像を見ると分かるようにcleanup tagは読者や編集者に記事の問題点を知らせている。
そしてこのようなcleanup tagはテンプレートから作成されており、その種類は320000ほど存在する。
そこでcleanup tagと記事の内容をSQLでwikipediaから取得し、最も頻度の高い10個のcleanup tagを用いて予測を行う。
分類にはOptimistic Setting
と Pessimistic Setting
というモデルを作成し、SVMを使い分類を行っている。
横軸は閾値で縦軸は精度と再現率となっているが精度ではOptimistic
の方が明らかに高いことが見て分かる。
また、cleanup tagの種類ごとに精度を見てみると、Orphan
は常に精度は1で、それ以外は記事の欠落の比率が大きくなるにつれて精度が低くなっているのが分かる。
Orphan
の定義自体リンクの個数といった具体的なもので表されるため、このような高い精度になったと考えられる。
下記はOrphan
タグの定義
結論
結果を見てみると多くの欠落が記事に含まれているとどういったcleanup tagが現れるのかという精度が低くなってしまうが、逆に欠落の少ない記事であれば高い精度がでている。
とくにリンクの数が少ないことを表すOrphan
というタグに至っては、記事の欠落の比率にかかわらず常に精度は1である。
それぞれのタグをif else文のみで完結に定義しているのにこのような高い精度を出しているのは感嘆する。
この論文をもとに今後、悪質な記事が減ることを期待する。
記事の質をcleanup tagを用いて解析したのは新規性のある素晴らしいアイデアだと思うが、このようなtagはwikipediaにしかついていないので応用性が乏しいのではないかと考える。