遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

ライブラリに依存するプロジェクトのテストプロジェクトをantで実行できない件+AOSPにパッチを送ってみた件

Android SDK r14以降で、テストプロジェクトおよび、テスト対象の本体プロジェクトの両方がライブラリプロジェクトのクラスを呼び出している場合、antでのテスト実行で実行が失敗します。このままではJenkinsでテストを自動実行するときなどに困ってしまうので、Android SDKbuild.xmlを書き換えて対応します。

以下この問題の詳細についてです。

問題の概要

問題が起こるのは、以下のようなプロジェクト構成の場合です。

  • ライブラリプロジェクト
  • 本体プロジェクト(ライブラリプロジェクトのクラスを呼び出す)
  • テストプロジェクト(ライブラリプロジェクトのクラスを呼び出す)

このようなプロジェクト構成をビルドしようとした場合に、取り得る手段は2通り考えられます。

  1. 本体プロジェクトのビルドパスにライブラリプロジェクトを追加して、本体プロジェクト側でライブラリプロジェクトをExportすることでテストプロジェクトは、ライブラリプロジェクトを参照させる
  2. 本体プロジェクト・テストプロジェクト両方のビルドパスにライブラリプロジェクトを追加する

ただ、これは両方ともうまくいきません。1はEclipse上ではうまく動きますが、antビルド時にはテストプロジェクトのコンパイルエラーが発生します。2はantテスト実行の際に、実行時例外が発生します。以下詳細を見てみます。

1のExport設定はEclipseのビルドパス設定で行うことができます。

この設定を行えば、Eclipse上でのテスト実行はうまくいきます。しかし、このExport設定が有効なのはEclipse上に限った話で、antからビルドしする際には読み込んでくれません。
そのため、テストプロジェクトをantでビルドしようとするとクラス解決がうまくいかず、コンパイルエラーになります。

$ ant clean debug install test 
-compile:
    [javac] Compiling 2 source files to /workspace/AndroidExampleTest/bin/classes
    [javac] /workspace/AndroidExampleTest/src/com/polysfactory/antbuild/sample/test/TestCaseClass.java:5: シンボルを見つけられません。
    [javac] シンボル: クラス LibraryClass
    [javac] 場所    : com.polysfactory.andbuild.library の パッケージ
    [javac] import com.polysfactory.andbuild.library.LibraryClass;
    [javac]                                         ^
    [javac] /workspace/AndroidExampleTest/src/com/polysfactory/antbuild/sample/test/TestCaseClass.java:10: シンボルを見つけられません。
    [javac] シンボル: 変数 LibraryClass
    [javac] 場所    : com.polysfactory.antbuild.sample.test.TestCaseClass の クラス
    [javac]         String s1 = LibraryClass.test();
    [javac]                     ^
    [javac] エラー 2 個

2の両方にライブラリプロジェクトを参照する方法は、Eclipseでもantでも、テスト実行に失敗します。
以下のような実行時例外が発生するはずです。

test:
     [echo] Running tests ...
     [exec] 
     [exec] com.polysfactory.antbuild.sample.test.TestCaseClass:.
     [exec] Error in testCase:
     [exec] java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
     [exec] 	at com.polysfactory.antbuild.sample.LibraryClassCaller.callLibraryClass(LibraryClassCaller.java:7)
     [exec] 	at com.polysfactory.antbuild.sample.test.TestCaseClass.testCase(TestCaseClass.java:12)
     [exec] 	at java.lang.reflect.Method.invokeNative(Native Method)
     [exec] 	at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:169)
     [exec] 	at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:154)
     [exec] 	at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:529)
     [exec] 	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1448)
     [exec] 
     [exec] Test results for InstrumentationTestRunner=..E
     [exec] Time: 0.086
     [exec] 
     [exec] FAILURES!!!
     [exec] Tests run: 2,  Failures: 0,  Errors: 1
     [exec] 
     [exec] 

これは、本体プロジェクトから参照しているライブラリクラスと、テストプロジェクトから参照しているライブラリクラスが異なるために発生するエラーです。apkを解凍してみれば分かりますが、ライブラリプロジェクトのクラス群が本体プロジェクトのapkと、テストプロジェクトのapkの両方に含まれてしまっています。

Android SDK r13以前ではこの方法でうまくいっていました。Android SDK r14からAndroidのライブラリプロジェクトの扱い方が変わり、このような現象が発生するようになったようです。

ライブラリプロジェクトのクラス群は、本体apkのみに含まれて、テストapkには含まれないのが正しい動きです。しかしAndroid SDKに含まれるbuild.xmlは、そのようなビルドプロセスをサポートしていない為、この問題を解決するにはAndroid SDKbuild.xmlを書き換える必要があります。

解決方法

build.xmlを書き換えます。antのビルドプロセスとして、テストプロジェクトのビルド->subantの呼び出しで本体プロジェクトのビルド->subantの呼び出しでライブラリプロジェクトのビルドとなっていますが、antではsubantの呼び出しをまたいで変数を共有できないので、一時ファイルにライブラリプロジェクトのjarのパスを保存しておくアプローチを取ります。

↓r16のbuild.xmlをベースにしたdiffファイル

<     <!-- Libraries property file -->
<     <property name="out.libraries.prop.file.name" value="libraries.prop" />
452c450
<                 
---
> 
524d521
< 
554,571d550
< 
<         <!-- save libraries' jar paths to a temporary file  -->
<         <propertyfile file="${out.absolute.dir}/${out.libraries.prop.file.name}">
< 	        <entry key="libraries.jars" value="${toString:project.libraries.jars}" />
<         </propertyfile>
< 
<         <!-- load tested project's libraries path if this is test project -->
<         <if>
<             <condition>
<                 <and>
<                     <isset property="tested.project.absolute.dir" />
<                 </and>
<             </condition>
<             <then>
<                 <property file="${tested.project.absolute.dir}/${out.dir}/${out.libraries.prop.file.name}"/>
<             </then>
<         </if>
< 
626d604
< 
628c606
<                     value="${tested.project.absolute.dir}/bin/classes;${libraries.jars}"
---
>                     value="${tested.project.absolute.dir}/bin/classes"

AOSPにパッチ送ってみた

せっかくAndroid SDKを修正したので、これを機にAndroid Open Source Projectにパッチを送ってみました。もし応援してくれる人がいたらスターとかつけちゃってください!
https://android-review.googlesource.com/33230