Gradleでapp.imlを操作するのがうまくいかない。

Androidでちょっと大きめな開発に携わっていて、2ヶ月半ぐらいどっぷりと。ずっとEclipseで書いてましたがその間にAndroid Studioも1.0がリリースされて今は1.0.2。たまに息抜きにAndroid Studioを触ってみたら、いきなり黒魔術のようなファイルがありました。そう、Gradleのビルドファイルです。 単にアプリのコード書くだけだったらビルドの調整もほとんど必要無いのですが、それ以外にちょっと何かしようとすると黒魔術が立ちはだかることがあります。一度行く手を遮られるとどうにも進まない。意味わからんでググっても世の中ほとんどの人がわかってない様子のためにどうにも捗らないから、ゼロから入門してみましたよ。言語はGroovyですか、あったなあそんなの。

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

日本のAmazonでこの本ポチって輸入して、とりあえず7章まで読んで、Gradleの入り口のところはテキトーデタラメだけど分かってきた。8章はテストのことで9章がAndroid開発、10章がIDEとの連携ってこれらを拾い読みながら、テスト書いてみたら。。。JUnit3.8ですね。。。Android固有のところはSDKドキュメントを読みながらすすめてみたら。。。動きませんね。テストがエミュレータ立ち上げに行こうとするあたりでどっか行っちゃう。しかも遅い。Javaコンパイル〜Dexコンパイル〜Apkにアーカイブ〜エミュレータへ転送コピー〜起動待ち、というそれぞれ重い処理を重ねるのでとても遅い。

解決方法を調べはじめてみると、エミュレータを使わないヘッドレスなテスト環境としてRoboletricなるものがJUnit4ベースであるのを発見。ウンウンこういうの好きだよ。3.8を放り出して早速こっちをちょっと書いてみたらすぐに動かせました。美しく機能も多い見かけなので今後はRoboに付き合っていくことにします。でも動いたとは言え、ターミナルからgradlewコマンドによるものだけ。Android StudioのUIからはいろいろ問題が積み上がっていて動かない。

いろいろ拗らせるなかで、こんなビルドスクリプト書いてました。が、しかしこれも思うようには動かない!

apply plugin: 'idea'
idea.module {
    testOutputDir = file('build/test-classes')
    iml {
        withXml { provider ->
            def comp = provider.node.component.find { it.@name = 'NewModuleRootManager' }
            if (comp != null) {
                def jdkEntry = comp.orderEntry.find { it.@type = 'jdk' }
                if(jdkEntry != null) {
                    comp.orderEntry.remove(jdkEntry) // 削るのは成功
                    comp.orderEntry.add(jdkEntry) // 失敗。うまく末尾に付け足せない
                }
            }
        }
    }
}
test.dependsOn ideaModule

ノード削って足しなおすところ、jdkEntry.@jdkNameやjdkEntry.@jdkTypeあたりでゼロからノードを作るべきなのかなと、ノードを改めて作る意図の下記でもダメだった。

comp.orderEntry.remove jdkEntry // 削るのは成功
comp.appendNode 'orderEntry',
         [type: 'jdk', jdkName: jdkEntry.@jdkName, jdkType: jdkEntry.@jdkType]

jdkを動かさずにTESTの方を動かすというまさにダメもと発想のこれもやっぱりダメ。フックでぶっ込んだ後、ファイルに書き出すところでtype="jdk"のものの書き出しがなんか違う。

if (comp != null) {
    def stack  = new java.util.Stack()
    for (def entry : comp.orderEntry) {
        if(entry.@scope == 'TEST') {
            stack.push entry
            comp.orderEntry.remove entry
        }
    }
    while (!stack.isEmpty()) {
        comp.orderEntry.add 1, stack.pop()
    }
}

なんでこんなの書いてるかを説明なしだと意味わかんないと思うけど、要はGradleスクリプトからAndroid Studioの設定ファイルであるapp.imlにあるライブラリ参照順においてSDKが先に来るとRoboが破綻しちゃう問題を解決しようと思っていろいろやってました。参照の早い順目からSDKを削るはいいが、末尾に付け直そうと思ったらなんか変になっちゃったというところで今日はタイムオーバー。

未解決だけど、どっかにスニペットをとっとこうかと思って、ここに。Android Studioは残念ながら息抜きなのでまた今度。

末尾に、いま、ターミナルからなら動くGradleスクリプトを貼っときます。

// プロジェクトの方のbuild.gradle
buildscript {
    repositories {
        jcenter() // mavenCentralよりこっちのほうが流行りらしい?
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.0.0'
        classpath 'org.robolectric:robolectric-gradle-plugin:0.14.1'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}
// appモジュールの方のbuild.gradle
apply plugin: 'com.android.application'
android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"
    defaultConfig {
        applicationId "com.glabio.sample"
        minSdkVersion 16
        targetSdkVersion 18 // Robo2.4はAPI18までしか対応してないよ!19&21は3.0で対応
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile 'junit:junit:4.12'
    androidTestCompile('org.robolectric:robolectric:2.4') {
        exclude group: 'commons-logging'  // これをやらないと毎回ワーニングが出る
        exclude group: 'org.apache.httpcomponents'
    }
}

apply plugin: 'robolectric'
robolectric {
    include '**/*Test.class'
    exclude '**/espresso/**/*.class'
    maxHeapSize = '2048m'
    jvmArgs '-XX:MaxPermSize=512m', '-XX:-UseSplitVerifier'
    maxParallelForks = 4
    forkEvery = 150
    ignoreFailures true
    afterTest { descriptor, result ->
        println "Executing test for ${descriptor.name} with result: ${result.resultType}"
    } // このafterTestクロージャがないと成功したテスト結果がコンソールに出力されない
}

そしてテストはこんな感じ。素敵。エミュレータ使わずともActivityを起動することができて、しかもJavaコンパイルJVM上での動作だけなのでSDK標準の方法とは比べ物にならない迅速な開発サイクル。

package com.glabio.sample;

import android.app.Activity;
import android.widget.TextView;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class MainActivityTest {
    @Test
    public void helloWorld() {
        Activity activity = Robolectric.buildActivity(MainActivity.class).create().get();
        TextView text = (TextView)activity.findViewById(R.id.textView);
        Assert.assertEquals(text.getText(), "Hello world!");
    }
}