tsurutanのつぶやき

備忘録としてつぶやきます

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 の編集

.babelrcpluginistanbul を追加します。

{
  "presets": ["@babel/preset-react"],
  "plugins": ["transform-class-properties", "istanbul"]
}

3.cypress/support/index.js の編集

cypress/support/index.jsimport '@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.jsmodue.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アプリで

github.com

などのgemを使うことで、IP制限を行うことができるのですがdynoへの負荷やアプリのパフォーマンス低下(上記だとRack::Attack.callが大量に呼ばれる)へと繋がるため、あまり良い選択ではないと思いました。 理想は許可したIPのみdynoへアクセスさせたいので、ネットワークでうまくできないかと調べたところCloudFlareを使えばうまくできそうだということがわかりました。

以下CloudFlareでの設定方法を説明します

CloudFlare

まずはCloudFlareで会員登録

f:id:tsurutan:20181106143528p:plain

次に登録したいサイトを入力します

f:id:tsurutan:20181106143537p:plain

60秒ほど待つとCNAMEのvalueが解析されるのでContinueを押します

f:id:tsurutan:20181106143607p:plain

そしてCloudFlareのネームサーバーの値が表示されるので、これを使用しているドメインに設定します。

f:id:tsurutan:20181106143616p:plain

ちなみにお名前ドットコムでは下記を編集します。

f:id:tsurutan:20181106144909p:plain

f:id:tsurutan:20181106144940p:plain

f:id:tsurutan:20181106145049p:plain

だいたい登録して1,2hで反映されましたが長いときには1日かかるときもあるそうです。

CloudFlareでのIP制限

CloudFlareでIP制限を行うためにはFirewallの設定を行います。 CloudFlare Firewallでは

  • Cookie
  • AS Num
  • Country
  • Host Name
  • IP Adress
  • Referer
  • Request Method etc

などのフィールドを元に制限ができ非常に便利です。 今回は日本以外のアクセスを制限したかったため下記のように書きました。

f:id:tsurutan:20181106144334p:plain

普通国外を制限する場合は日本のIPアドレスすべて書き込む必要があるのですが、CloudFlareではたったこれだけで制限ができるので非常に便利ですね。

また国外にてどう表示されているかを確認する際は下記のchrome拡張を使うとスムーズに確認できます。

www.betternet.co

実際にサイトを見てみると

f:id:tsurutan:20181106145345p:plain

正常に制限されていることがわかりますね。

CloudFlareは今までCDNなどに用いるイメージがありましたが、無料で且つ簡単にIP制限ができるなんて素晴らしいサービスですね!

現場で使える Ruby on Rails 5速習実践ガイド(特典PDF付き)

現場で使える Ruby on Rails 5速習実践ガイド(特典PDF付き)

  • 作者: 大場寧子,松本拓也,櫻井達生,小田井優,大塚隆弘,依光奏江,銭神裕宜,小芝美由紀
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2018/10/19
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5アプリケーションプログラミング

プロフェッショナルのための 実践Heroku入門 プラットフォーム・クラウドを活用したアプリケーション開発と運用 (書籍)

プロフェッショナルのための 実践Heroku入門 プラットフォーム・クラウドを活用したアプリケーション開発と運用 (書籍)

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の型として intboolean などのプリミティブ型を指定するとデフォルトで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

使用方法

  1. 上記スクリプトを diff.sh という名前で保存。
  2. jqを使うので、なければbrew install jqコマンドを打ってインストール。
  3. エラーログをerror.txtというファイルに保存。
  4. 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が返されていることがわかります。 またmApplicationattachメソッドが呼び出された時に初期化されています。

またActivity.attachActivityThread.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     }

全体コード(http://tools.oesf.biz/android-7.1.0_r1.0/xref/frameworks/base/core/java/android/app/ActivityThread.java)

   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.classNamenullの時に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     }

全体コード(http://tools.oesf.biz/android-7.1.0_r1.0/xref/frameworks/base/core/java/android/app/Instrumentation.java)

   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()を子のフラグメントで直接呼び出すとライフサイクルを無視し、バックグラウンドに存在するフラグメントが表示されないというバグが発生してしまったことがわかります。

ほんきで学ぶAndroidアプリ開発入門 第2版 Android Studio、Android SDK 7対応

ほんきで学ぶAndroidアプリ開発入門 第2版 Android Studio、Android SDK 7対応

Combining Textual Entailment and Argumentation Theory for Supporting Online Debates Interactions の概要と考察

f:id:tsurutan:20161016154131j:plain

今回は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

序章

f:id:tsurutan:20161017154856j:plain

近年、TwitterやDebetepediaといったサービスで様々な話題で多くの議論が交わせれている。

www.debatepedia.org

しかし、そのような話題に途中で参加するには今までの議論を一つ一つ見ていき、議論が収束しているのか、また今後話し合う必要があるところなどを考えなくてはいけなく、それを面倒に感じることが多いのではないかと考えている。

そこでこの論文ではこれらのサービスで繰り広げられている議論を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に対してなのかを区別し最終的にどこで議論が収束していたのか見ている。

f:id:tsurutan:20161017160255p:plain

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を用いている。

f:id:tsurutan:20161017161506p:plain

結果を見てみると既存の手法では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件) を見る

言語処理のための機械学習入門 (自然言語処理シリーズ)

言語処理のための機械学習入門 (自然言語処理シリーズ)

自然言語処理の基礎

自然言語処理の基礎

Predicting Quality Flaws in User-generated Content: The Case of Wikipedia の概要と考察

f:id:tsurutan:20161016154131j:plain

2012年にACMというカンファレンスに出された「Predicting Quality Flaws in User-generated Content: The Case of Wikipedia」という論文を読み考察をしました。

www.acm.org

[原文] http://www.uni-weimar.de/medien/webis/publications/papers/stein_2012i.pdf

この論文ではWikipediaについているcleanup tagをもとに記事の質の悪さを予測する手法を提案している。

序章

現在誰もが知っているWikipediaには日々膨大な記事が作成されていおり、その著者に誰もがなれるため様々な質の記事が生み出されている。

本来は記事がリリースされる前に専門家のチェックを理想とするが、記事の量が膨大なため人手で目を通すのは不可能に近い。

そこで、この論文では記事の良し悪しを見るためcleanup tagを用いて、コンピューターが記事の欠落を予測する手法を提案している。

実験

cleanup tagとはWikipediaの記事の欠落を表すタグである。

下記の画像を見ると分かるようにcleanup tagは読者や編集者に記事の問題点を知らせている。

f:id:tsurutan:20161015144829p:plain

そしてこのようなcleanup tagはテンプレートから作成されており、その種類は320000ほど存在する。

そこでcleanup tagと記事の内容をSQLでwikipediaから取得し、最も頻度の高い10個のcleanup tagを用いて予測を行う。

f:id:tsurutan:20161015150846p:plain

分類にはOptimistic SettingPessimistic Settingというモデルを作成し、SVMを使い分類を行っている。

f:id:tsurutan:20161016152546p:plain

横軸は閾値で縦軸は精度と再現率となっているが精度ではOptimisticの方が明らかに高いことが見て分かる。

また、cleanup tagの種類ごとに精度を見てみると、Orphanは常に精度は1で、それ以外は記事の欠落の比率が大きくなるにつれて精度が低くなっているのが分かる。

f:id:tsurutan:20161015153352p:plain

f:id:tsurutan:20161015153415p:plain

Orphanの定義自体リンクの個数といった具体的なもので表されるため、このような高い精度になったと考えられる。

下記はOrphanタグの定義

f:id:tsurutan:20161016153622p:plain

結論

結果を見てみると多くの欠落が記事に含まれているとどういったcleanup tagが現れるのかという精度が低くなってしまうが、逆に欠落の少ない記事であれば高い精度がでている。

とくにリンクの数が少ないことを表すOrphanというタグに至っては、記事の欠落の比率にかかわらず常に精度は1である。

それぞれのタグをif else文のみで完結に定義しているのにこのような高い精度を出しているのは感嘆する。

この論文をもとに今後、悪質な記事が減ることを期待する。

記事の質をcleanup tagを用いて解析したのは新規性のある素晴らしいアイデアだと思うが、このようなtagはwikipediaにしかついていないので応用性が乏しいのではないかと考える。