25

I've generated a native library using ndk-build which I'm able to load and use with in my Android application. However, I want to write some tests against this part of my app.

When calling the native function in my tests, I recieve this exception message:

java.lang.UnsatisfiedLinkError: no process in java.library.path

...where process is my native library to import, named libprocess.so.

I'm using Roboelectric for my tests, and running this particular one with the RobolectricTestRunner, if it makes a difference.

How can I get my test project to 'see' the native library?


Edit: I'm loading the library in my app like so:

static {
    System.loadLibrary("process");
}
public static native int[] process(double[][] data);

calling Process.process(array) works fine in the app (the library is loaded), but fails when run from the tests with the exception given above.


Edit 2: If I set -Djava.library.path="<the directory of libprocess.so>" as a VM argument, then:

System.out.println(System.getProperty("java.library.path"));

does show the path I set, but I still get the same exception. I'm setting the directory as:

<project-name>/libs/x86

...but as an absolute path.

blork
  • 2,150
  • 6
  • 26
  • 45
  • Do you have your package name correct? How are you loading your library? – Sam R. May 14 '13 at 10:50
  • I've added to my question, see above. – blork May 14 '13 at 10:55
  • Would you test with this: `-Djava.library.path=""`? – Sam R. May 14 '13 at 11:01
  • Thakns for your help, but adding that to "VM arguments" under the Eclipse Run Configuration options leaves me with the same error. – blork May 14 '13 at 11:52
  • Do you use any repository? Any possibility I can see your code? – Sam R. May 14 '13 at 12:00
  • Sorry, I can't make the source available. – blork May 14 '13 at 12:21
  • Last suggestion, Add the path to `libprocess.so` to your `PATH` environment variable and test again. – Sam R. May 14 '13 at 12:36
  • No luck there, either. – blork May 14 '13 at 12:52
  • What is your build/test environment? If it's not Linux, you have a problem. But even on Linux, you must build the native library with Linux host libraries, not Android libs. – Alex Cohn May 14 '13 at 14:12
  • I'm running Mac OS X, and I have x86 binaries. I think you might be right about the libraries being incompatible. I've switched form RoboElectric to the default tests (ActivityUnitTestCase, running in the emulator) and it's now working for me. – blork May 15 '13 at 10:39

4 Answers4

17

For anyone still looking, blork had the right idea - you need to compile your native libraries for your 'native' platform (Windows, Linux, Mac). The Android NDK builds libraries for the Android platform (.so files - might also work on Linux), and this is why there are no issues running in Activity Test Cases (because it loads up an Android instance).

To get the low-level, hella fast JUnit tests running, you need to support your JVM. On Windows, this might be building DLLs, on Apple, it's building dylibs (assuming shared libraries).

I've just completed a sample in my android-ndk-swig-example repo (https://github.com/sureshjoshi/android-ndk-swig-example/issues/9).

Basically, in my CMakeLists, I added an Apple caveat:

# Need to create the .dylib and .jnilib files in order to run JUnit tests
if (APPLE)
    # Ensure jni.h is found
    find_package(JNI REQUIRED)
    include_directories(${JAVA_INCLUDE_PATH})

And then I make sure Gradle runs for unit tests, but using the Mac build system (not NDK).

def osxDir = projectDir.absolutePath + '/.externalNativeBuild/cmake/debug/osx/'

task createBuildDir() {
    def folder = new File(osxDir)
    if (!folder.exists()) {
        folder.mkdirs()
    }
}

task runCMake(type: Exec) {
    dependsOn createBuildDir
    workingDir osxDir // Jump to future build directory
    commandLine '/usr/local/bin/cmake' // Path from HomeBrew installation
    args '../../../../' // Relative path for out-of-source builds
}

task runMake(type: Exec) {
    dependsOn runCMake
    workingDir osxDir
    commandLine 'make'
}

 project.afterEvaluate {
    // Not sure how much of a hack this is - but it allows CMake/SWIG to run before Android Studio
    // complains about missing generated files
    // TODO: Probably need a release hook too?
    javaPreCompileDebug.dependsOn externalNativeBuildDebug
    if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) {
        javaPreCompileDebugAndroidTest.dependsOn runMake
    }
 }

CAVEAT TIME!!!

When you use this method, you're technically not testing the NDK-generated libs. You're testing the same code, but compiled using a different compiler (msvc, xcode, gcc, clang, whatever you use on the host).

What this means practically, is that most of the test results will be valid - except when you run into problems caused by each compiler's quirks, or STL implementations, etc... This isn't as bad as it was 10+ years ago, but you can't say with 100% certainty that the results of JUnit testing with host libs is identical to the Android libs. You can say that it's reasonably close, though.

Then again, unless you're running your native unit tests using the Android NDK for each supported architecture, you also can't say anything about certainty either... So take what you will from it.

An overkill approach (but really cool if automated) would be to write your native unit tests however you do them (Google Test, Catch, etc), then compile and run your native libs and unit tests with the Android NDK per each architecture. This provides your C/C++ coverage across your potential target architectures.

From here, you could use the aforementioned host libs with JUnit to rapidly unit test your JNI layer interacting with your native lib. In your CI system, you should still probably run these same unit tests - but as Android Instrumentation tests (or something else that runs an emulated Android environment).

As with everything, wherever you have an interface, you can create mocks - but at some point, you'll need system/functional/integration tests too.

Update:

More comprehensive explanation of above in a blog article (http://www.sureshjoshi.com/mobile/android-junit-native-libraries/)

SJoshi
  • 1,866
  • 24
  • 47
  • 2
    What should I change in order to get it for Windows? (64 bits) – Pregunton May 14 '19 at 21:19
  • @Pregunton Well, probably most of that build script - as everything is setup with *nix style arguments and directory structures. You'd have to have cmake in your PATH and then setup the correct Windows command line calls. I'm sure there is a cross-platform way to do all that. The CMakeLists might need a couple of tweaks, as well - just can't recall since I did this a couple years ago. – SJoshi May 15 '19 at 12:55
  • I like this answer and would like to add one correction. The Android NDK builds libraries for the Android platform (.so files - might also work on Linux), >> This is NOT true as .so is pure platform-specific i.e. x86 or ARM or other platform. – siva Jul 06 '20 at 04:20
  • @SJoshi can you please have a look on this https://stackoverflow.com/questions/72238895/cmake-building-dylib-to-test-android-junit-tests – Vivek Mangal May 14 '22 at 09:56
1

I've switched to the default testing style (using ActivityUnitTestCase instead of RoboElectric) and it's now running fine. It's a shame I have to sacrifice the speed of test running, but running the tests on the emulator actually works. You could also create a shadow class for the JNI class, as detailed here:

Robolectric tanks on Application objects that load JNI libraries. Can I get a workaround?

Perhaps compiling the library for my machine would have worked, but I couldn't spend any more time on it.

Community
  • 1
  • 1
blork
  • 2,150
  • 6
  • 26
  • 45
1

Just in case, for anyone else looking for the solutions to this, there is a way to properly set the lib lookup path for JUnit tests (although I still haven't managed to successfully run the tests, different set of issues):

You need to put the path in the LD_LIBRARY_PATH (or PATH on Windows) environment variable. I had the exact same experience: I set the java.library.path first, but seemingly it doesn't affect the native loader's behavior in any way.

You're welcome to take a look at my answer here if you need details on how to do that with Gradle/Android Studio

Ivan Bartsov
  • 19,664
  • 7
  • 61
  • 59
0

Add the following lined to CMakeLists.txt works for me.

set(JAVA_AWT_LIBRARY NotNeeded)
set(JAVA_JVM_LIBRARY NotNeeded)
set(JAVA_INCLUDE_PATH2 NotNeeded)
set(JAVA_AWT_INCLUDE_PATH NotNeeded)
find_package(JNI REQUIRED)