Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android unit test using ant with library project

It seems that also the latest android SDK tools still don't properly support testing of applications that contain linked library projects.

I have a project with the following setup:

TestLib (android library project) <- TestMain (android project) <- TestMainTest (android unit test project)

I created all those projects in eclipse and then used android update (test-/lib-)project ... to generate the build.xml et. al.

The problem starts as soon as you have a class in TestMain (InheritAddition.java in my example) that inherits from a class in TestLib (Addition.java) and you want to reference this class in the unit test (InheritAdditionTest.java).

TestLib

public class Addition {
    public int add2(int o1, int o2) {
      return o1 + o2;
    }
}

TestMain

public class InheritAddition extends Addition {
    public int sub(int p1, int p2) {
        return p1 - p2;
    }
}

TestMainTest

public class InheritAdditionTest extends AndroidTestCase {
    public void testSub() {
        Assert.assertEquals(2, new InheritAddition().sub(3, 1));
    }
}

When building on the command line the result is the following:

W/ClassPathPackageInfoSource(14871): Caused by: java.lang.NoClassDefFoundError: org/test/main/InheritAddition
W/ClassPathPackageInfoSource(14871):    ... 26 more
W/ClassPathPackageInfoSource(14871): Caused by: java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
W/ClassPathPackageInfoSource(14871):    at dalvik.system.DexFile.defineClass(Native Method)
W/ClassPathPackageInfoSource(14871):    at dalvik.system.DexFile.loadClassBinaryName(DexFile.java:195)
W/ClassPathPackageInfoSource(14871):    at dalvik.system.DexPathList.findClass(DexPathList.java:315)
W/ClassPathPackageInfoSource(14871):    at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:58)
W/ClassPathPackageInfoSource(14871):    at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
W/ClassPathPackageInfoSource(14871):    at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
W/ClassPathPackageInfoSource(14871):    ... 26 more
W/dalvikvm(14871): Class resolved by unexpected DEX: Lorg/test/main/InheritAddition;(0x41356250):0x13772e0 ref [Lorg/test/lib/Addition;] Lorg/test/lib/Addition;(0x41356250):0x13ba910

I found some workaround that works for eclipse:

Can't build and run an android test project created using "ant create test-project" when tested project has jars in libs directory

That does the trick, but I am looking for a solution that works with ANT (more precisely I am looking for a solution that works on both at the same time).

The documented approach (by changing build.xml to include jars from the main project into the class path) is not applicable here as the sample project doesn't use any library jars (also I believe that this particular problem is now fixed with SDK tools r16).

I guess the brute force way of solving that is to try and somehow remove the dependencies of TestMainTest to TestLib (by modifying project.properties) and instead manage to hack the build script to put those built jars into the class path (so replace the -compile target with something that modifies the class path for javac). Since I have a long history of trying to keep up with android SDK toolchain changes, this is not really my favorite option as it is a) rather complicated and b) requires constant modification of the build.xml whenever the toolchain changes (which is quite frequently).

So I am looking for ideas of how to get such a setup working without using the sledge hammer. Maybe I am missing something totally obvious but for me this use case is fairly standard and I have a hard time understanding why this isn't supported out of the box.

like image 440
RaB Avatar asked Dec 21 '11 19:12

RaB


2 Answers

Using Android SDK Tools r15 and Eclipse.

Suppose you create three projects in eclipse: Lib (android library project) <- App (android application project) <- Test (android unit test project) and define the following classes:

[Lib]

public class A {}

[App]

public class A extends B {}

[Test]

public class MyUnitTest extends AndroidTestCase {
    public void test() {
        new A();
        new B();
    }
}

In this setup TestMain references TestLib as an Android library and TestMainTest has a project reference to TestMain.

You should see that Test doesn’t compile because A cannot be resolved. This is expected because Test has no visibility into Lib. One solution is to add a library reference from Test to Lib. While this fixes the compile problem, it breaks at run time. A bunch of errors result, but this is the interesting one:

W/dalvikvm( 9275): Class resolved by unexpected DEX: Lcom/example/B;(0x40513450):0x294c70 ref [Lcom/example/A;] Lcom/example/A;(0x40513450):0x8f600
W/dalvikvm( 9275): (Lcom/example/B; had used a different Lcom/example/A; during pre-verification)
W/dalvikvm( 9275): Unable to resolve superclass of Lcom/example/B; (1)
W/dalvikvm( 9275): Link of class 'Lcom/example/B;' failed
E/dalvikvm( 9275): Could not find class 'com.example.B', referenced from method com.example.test.MyUnitTest.test
W/dalvikvm( 9275): VFY: unable to resolve new-instance 3 (Lcom/example/B;) in Lcom/example/test/MyUnitTest;
D/dalvikvm( 9275): VFY: replacing opcode 0x22 at 0x0000
D/dalvikvm( 9275): VFY: dead code 0x0002-000a in Lcom/example/test/MyUnitTest;.test ()V

This is because both the Test and App projects reference the Lib library project, so both resulting apks include a copy of com.example.A.

Do not add explicit dependencies in eclipse from a test project to a library project (if that library is a dependency of the application project under test). Doing so can cause both the application and test projects to include copies of the same classes in their resulting apks and the test will fail at run time.

We need to find a way around the compile time visibility issue. At run time, Test will have visibility into App and therefore the classes in Lib. Instead of creating a library reference from Test to Lib, update the build path of App to export it’s library projects. Now Test compiles and the unit test successfully runs.

In Eclipse, to test an application project that references a library project, export the library projects from the application project in it’s build path settings.

Everything works in Eclipse now, but that about Ant? Use the android update [lib-|test-]project commands to create the necessary build.xml files. Be sure to run Ant clean in all three directories: Lib, App, and Test. Failure to clean all three projects may result in a successful compile.

The Ant compile will fail with:

[javac] ...Test/src/com/example/test/MyUnitTest.java:3: cannot find symbol
[javac] symbol  : class A
[javac] location: package com.example
[javac] import com.example.A;
[javac]                   ^
[javac] ...Test/src/com/example/test/MyUnitTest.java:10: cannot access com.example.A
[javac] class file for com.example.A not found
[javac]         new B();
[javac]         ^
[javac] ...Test/src/com/example/test/MyUnitTest.java:11: cannot find symbol
[javac] symbol  : class A
[javac] location: class com.example.test.MyUnitTest
[javac]         new A();
[javac]             ^
[javac] 3 errors

Why does the Ant build fail when the Eclipse build succeeds? The Eclipse and Ant build systems are distinct. Exporting the library projects from App in Eclipse has not effect on the Ant build. The build failed because the Test project doesn’t have visibility into the Lib project. If we try to solve this problem by adding an android.library.refernce property to Test/project.properties, we have done exactly the same thing as adding a library reference from Test to Lib in Eclipse. The Ant build would succeed, but the test would fail at run time with the familiar “Class resolved by unexpected DEX” error.

We need a way for the test project to compile against the library project, but not include it in the dexing process. There are two steps to this process. First, include a reference from Test to Lib that does not affect Eclipse. Second, update the Ant build system so the library is compiled against but in excluded from dexing.

At the top of Test/build.xml I define a property that points to the library. This is similar to adding a reference to Test/project.properties except that Eclipse won’t see it:

Now we need to exclude the library jar from the dexing process. This requires updating the dex-helper macro. I place the macro override after the line in my Test/build.xml file. The new dex-helper excludes all jar files not in the Test project folder tree from the dexing process:

<macrodef name="dex-helper">
  <element name="external-libs" optional="yes"/>
  <attribute name="nolocals" default="false"/>
  <sequential>
    <!-- sets the primary input for dex. If a pre-dex task sets it to
                 something else this has no effect -->
    <property name="out.dex.input.absolute.dir" value="${out.classes.absolute.dir}"/>
    <!-- set the secondary dx input: the project (and library) jar files
                 If a pre-dex task sets it to something else this has no effect -->
    <if>
      <condition>
        <isreference refid="out.dex.jar.input.ref"/>
      </condition>
      <else>
        <!--
                        out.dex.jar.input.ref is not set. Compile the list of jars to dex.
                        For test projects, only dex jar files included in the project
                        path
                    -->
        <if condition="${project.is.test}">
          <then>
            <!-- test project -->
            <pathconvert pathsep="," refid="jar.libs.ref" property="jars_to_dex_pattern"/>
            <path id="out.dex.jar.input.ref">
              <files includes="${jars_to_dex_pattern}">
                <!-- only include jar files actually in the test project -->
                <filename name="${basedir}/**/*"/>
              </files>
            </path>
            <property name="in_jars_to_dex" refid="jar.libs.ref"/>
            <property name="out_jars_to_dex" refid="out.dex.jar.input.ref"/>
            <echo message="Test project! Reducing jars to dex from ${in_jars_to_dex} to ${out_jars_to_dex}."/>
          </then>
          <else>
            <path id="out.dex.jar.input.ref"/>
          </else>
        </if>
      </else>
    </if>
    <dex executable="${dx}" output="${intermediate.dex.file}" nolocals="@{nolocals}" verbose="${verbose}">
      <path path="${out.dex.input.absolute.dir}"/>
      <path refid="out.dex.jar.input.ref"/>
      <external-libs/>
    </dex>
  </sequential>
</macrodef>

With these changes in place, Test builds and runs from both Eclipse and Ant.

Happy testing!

Other notes: If things are not building in Eclipse and you believe they should, try refreshing the the projects in the following order: Lib, App, Test. I frequently have to do this after making build path changes. I also sometimes have to build clean for things to work properly.

like image 116
wallacen60 Avatar answered Nov 20 '22 14:11

wallacen60


The answer of @wallacen60 is nice. I came to the same conclusion yesterday. Neverthe less, there is another option : instead of excluding the lib's jar from dexing of the test projet, it would be nice if we could find a way to include the lib's jar in the compilation (javac, compile stage of the ant file) of test, and only in the compilation stage and not the dexing stage.

The solution of @wallacen60 moreover introduces a big semantic difference between the compilation of the 3 project and their dependencies : in Eclipse App depends on lib, test depends on App. And that is the right way to do it. But in ant, both App and Test depend on Lib and seems like a bad redunduncy cycle to me.

So, for now, what we did was to patch the test project's project.properties file so that it includes this line :

tested.android.library.reference.1=../SDK_android

And we modified the ant file of the tested project so that the compile target includes the library : (look at the changed line, search for the word "change").

    <!-- override "compile" target in platform android_rules.xml to include tested app's external libraries -->
<!-- Compiles this project's .java files into .class files. -->
<target name="-compile" depends="-build-setup, -pre-build, -code-gen, -pre-compile">
    <do-only-if-manifest-hasCode elseText="hasCode = false. Skipping...">
        <!-- If android rules are used for a test project, its classpath should include
             tested project's location -->
        <condition property="extensible.classpath"
                value="${tested.project.absolute.dir}/bin/classes"
                else=".">
            <isset property="tested.project.absolute.dir" />
        </condition>
        <condition property="extensible.libs.classpath"
                value="${tested.project.absolute.dir}/${jar.libs.dir}"
                else="${jar.libs.dir}">
            <isset property="tested.project.absolute.dir" />
        </condition>
        <echo message="jar libs dir : ${tested.project.target.project.libraries.jars}"/>
        <javac encoding="${java.encoding}"
                source="${java.source}" target="${java.target}"
                debug="true" extdirs="" includeantruntime="false"
                destdir="${out.classes.absolute.dir}"
                bootclasspathref="android.target.classpath"
                verbose="${verbose}"
                classpath="${extensible.classpath}"
                classpathref="jar.libs.ref">
            <src path="${source.absolute.dir}" />
            <src path="${gen.absolute.dir}" />
            <classpath>
                <!-- steff: we changed one line here !-->
                <fileset dir="${tested.android.library.reference.1}/bin/" includes="*.jar"/>
                <fileset dir="${extensible.libs.classpath}" includes="*.jar" />
            </classpath>
            <compilerarg line="${java.compilerargs}" />
        </javac>
               <!-- if the project is instrumented, intrument the classes -->
                        <if condition="${build.is.instrumented}">
                            <then>
                                <echo>Instrumenting classes from ${out.absolute.dir}/classes...</echo>
                                <!-- It only instruments class files, not any external libs -->
                                <emma enabled="true">
                                    <instr verbosity="${verbosity}"
                                           mode="overwrite"
                                           instrpath="${out.absolute.dir}/classes"
                                           outdir="${out.absolute.dir}/classes">
                                    </instr>
                                    <!-- TODO: exclusion filters on R*.class and allowing custom exclusion from
                                         user defined file -->
                                </emma>
                            </then>
                        </if>           
    </do-only-if-manifest-hasCode>
</target>

Indeed, this mechanism seems to be the right one as it mimics what eclipse does. But eclipse is able to know that the App depends on lib when compiling test. The only difference is that we exposed this relation manually in ant via the line (in project.properties)

tested.android.library.reference.1=../SDK_android

But it could be possible to do that automatically. I can't find the mechanism that google ant tools use to produce the librairy project path refid from the android.library.* statement in a project.properties. But if I could find this mechanism I could propagate this dependency in the test project, as eclipse does.

So I think the best would be to let google know that they have a patch to do, and temporarily keep the solution of exporting manually the dependency of th app project toward the lib project in order to compile the test project.

Can someone contact google about this bug ?

like image 21
Snicolas Avatar answered Nov 20 '22 14:11

Snicolas