Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/android-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ runs:
if [ ${TARGET_ABI} == 'arm64-v8a' ]; then
bash ${{ github.workspace }}/java/build-nnstreamer-android.sh --target_abi=${TARGET_ABI} --enable_nnfw=yes --build_type=${BUILD_TYPE} --enable_mqtt=yes --enable_ml_offloading=yes --enable_ml_service=yes --enable_llamacpp=yes
elif [ ${TARGET_ABI} == 'x86_64' ]; then
bash ${{ github.workspace }}/java/build-nnstreamer-android.sh --target_abi=${TARGET_ABI} --enable_nnfw=no --build_type=${BUILD_TYPE}
bash ${{ github.workspace }}/java/build-nnstreamer-android.sh --target_abi=${TARGET_ABI} --enable_nnfw=no --build_type=${BUILD_TYPE} --build_test=yes
else
bash ${{ github.workspace }}/java/build-nnstreamer-android.sh --target_abi=${TARGET_ABI} --enable_nnfw=no --enable_tflite=no --build_type=${BUILD_TYPE}
fi
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/android-jni-test-ubuntu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Android JNI test on Ubuntu

# Build the JNI wrapper of the Android API on Ubuntu and run the instrumented tests
# that do not require an Android device (java/test-nnstreamer-ubuntu.sh).

on:
pull_request:
branches: [ main ]
paths:
- 'c/**'
- 'java/**'
- 'meson.build'
- 'meson_options.txt'
- '.github/workflows/android-jni-test-ubuntu.yml'

# Allow manually triggering the workflow
workflow_dispatch:

jobs:
test:
name: Android JNI host test
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install dependencies
run: |
for i in 1 2 3; do sudo add-apt-repository -y ppa:nnstreamer/ppa && break || sleep 30; done
sudo apt-get update
sudo apt-get install -y meson ninja-build \
libglib2.0-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
nnstreamer nnstreamer-dev nnstreamer-dev-internal nnstreamer-single-dev \
openjdk-17-jdk-headless junit4
- name: Build JNI wrapper
run: |
meson setup build -Djava-home=${JAVA_HOME} -Denable-test=false -Denable-ml-service=false
ninja -C build
- name: Run tests
run: |
bash java/test-nnstreamer-ubuntu.sh --ml_api_dir=${{ github.workspace }} --build_dir=build
20 changes: 20 additions & 0 deletions java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# NNStreamer API for Java

The Java API and its JNI wrapper for the Android API of NNStreamer.

- `android/`: the Android library project. `android/nnstreamer/src/main/jni` is the
JNI wrapper, which is also built on Ubuntu by meson when `java-home` is given.
- `build-nnstreamer-android.sh`: build the Android library (AAR). Use
`--build_test=yes` to build the instrumented tests as well, and `--run_test=yes`
to run them on a connected device.
- `build-nnstreamer-ubuntu.sh`: build the Java API as a jar for Ubuntu.
- `test-nnstreamer-ubuntu.sh`: run the instrumented tests that do not require an
Android device on a desktop JVM, against the JNI wrapper built by meson. The
tests that cannot run on the host are listed in `host-test/exclude.txt`, and the
Android classes they use are stubbed in `host-test/stub`.

```
meson setup build -Djava-home=$JAVA_HOME -Denable-test=false -Denable-ml-service=false
ninja -C build
bash java/test-nnstreamer-ubuntu.sh --ml_api_dir=. --build_dir=build
```
16 changes: 16 additions & 0 deletions java/build-nnstreamer-android.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
##@@ 'internal' : no plugins except for enable single-shot only, enable NNFW only
##@@ --target_abi=(armeabi-v7a|arm64-v8a)
##@@ 'arm64-v8a' is the default Android ABI
##@@ --build_test=(yes|no)
##@@ 'yes' : build instrumentation test after build procedure is done
##@@ 'no' : [default]
##@@ --run_test=(yes|no)
##@@ 'yes' : run instrumentation test after build procedure is done
##@@ 'no' : [default]
Expand Down Expand Up @@ -128,6 +131,9 @@ include_assets="no"
# Set target ABI ('armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64')
target_abi="arm64-v8a"

# Build instrumentation test after build procedure is done
build_test="no"

# Run instrumentation test after build procedure is done
run_test="no"

Expand Down Expand Up @@ -212,6 +218,9 @@ for arg in "$@"; do
--target_abi=*)
target_abi=${arg#*=}
;;
--build_test=*)
build_test=${arg#*=}
;;
--run_test=*)
run_test=${arg#*=}
;;
Expand Down Expand Up @@ -794,6 +803,13 @@ chmod +x gradlew
sh ./gradlew nnstreamer:build
android_lib_build_res=$?

# Build instrumentation test if build procedure is done.
if [[ ${android_lib_build_res} -eq 0 && ${build_test} == "yes" ]]; then
echo "Build instrumentation test."
sh ./gradlew nnstreamer:assembleDebugAndroidTest
android_lib_build_res=$?
fi

# Run instrumentation test if build procedure is done.
if [[ ${android_lib_build_res} -eq 0 ]]; then
if [[ ${run_test} == "yes" ]]; then
Expand Down
22 changes: 22 additions & 0 deletions java/host-test/exclude.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Instrumented tests that cannot run on the host, one per line: Class#method or Class.
# An entry that does not match any test fails the run, remove it with the test.

# Android application context is not checked on the host (@BUILD_ANDROID@).
org.nnsuite.nnstreamer.APITestCommon#testInitWithInvalidCtx_n

# amcsrc is an Android element.
org.nnsuite.nnstreamer.APITestPipeline#testAvailableElement
org.nnsuite.nnstreamer.APITestPipeline#testAMCsrc

# The video sink renders to an Android native window; glimagesink needs a display.
# Without one the pipeline does not start, so the negative cases below would pass
# on the failure of start() rather than on the call they are written for.
org.nnsuite.nnstreamer.APITestPipeline#testSetNullSurface
org.nnsuite.nnstreamer.APITestPipeline#testSetSurfaceNullName_n
org.nnsuite.nnstreamer.APITestPipeline#testSetSurfaceEmptyName_n
org.nnsuite.nnstreamer.APITestPipeline#testSetInvalidSurface_n

# Test assets (image, model) are copied from the Android test package.
org.nnsuite.nnstreamer.APITestPipeline#testAppsrcPng
org.nnsuite.nnstreamer.APITestSingleShot#testOptionsUnknownFW_n
org.nnsuite.nnstreamer.APITestSingleShot#testOptionsInvalidFW_n
111 changes: 111 additions & 0 deletions java/host-test/src/org/nnsuite/nnstreamer/HostTestRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* NNStreamer Android API host test
* Copyright (C) 2026 Samsung Electronics Co., Ltd.
*/

package org.nnsuite.nnstreamer;

import org.junit.internal.TextListener;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.manipulation.Filter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Runs the instrumented test classes on a desktop JVM, skipping the tests listed in an exclude file.
*
* Usage: HostTestRunner (exclude file) (test class name)...
*
* Each line of the exclude file is a class name, or a class name and a method name joined
* with '#'. Text after '#' at the start of a line or after whitespace is a comment.
* An entry that does not match any test fails the run, so that the list stays accurate.
*/
public final class HostTestRunner {
private static Set<String> readExcludeFile(String path) throws IOException {
Set<String> entries = new HashSet<>();

for (String line : Files.readAllLines(Paths.get(path), StandardCharsets.UTF_8)) {
int comment = line.indexOf(" #");
String entry = (comment >= 0 ? line.substring(0, comment) : line).trim();

if (!entry.isEmpty() && !entry.startsWith("#")) {
entries.add(entry);
}
}

return entries;
}

public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.err.println("Usage: HostTestRunner <exclude file> <test class>...");
System.exit(2);
}

final Set<String> excluded = readExcludeFile(args[0]);
final Set<String> matched = new HashSet<>();
List<Class<?>> classes = new ArrayList<>();

for (int i = 1; i < args.length; i++) {
Class<?> klass = Class.forName(args[i]);

if (excluded.contains(klass.getName())) {
matched.add(klass.getName());
System.out.println("Excluded class " + klass.getName());
} else {
classes.add(klass);
}
}

Filter filter = new Filter() {
@Override
public boolean shouldRun(Description description) {
if (!description.isTest()) {
return true;
}

String name = description.getClassName() + "#" + description.getMethodName();

if (excluded.contains(name)) {
matched.add(name);
return false;
}

return true;
}

@Override
public String describe() {
return "exclude tests listed in " + args[0];
}
};

JUnitCore core = new JUnitCore();
core.addListener(new TextListener(System.out));

Result result = core.run(Request.classes(classes.toArray(new Class<?>[0])).filterWith(filter));
boolean success = result.wasSuccessful();

System.out.println("Excluded " + (matched.size()) + " entries.");

for (String entry : excluded) {
if (!matched.contains(entry)) {
System.out.println("Exclude entry does not match any test: " + entry);
success = false;
}
}

System.exit(success ? 0 : 1);
}
}
43 changes: 43 additions & 0 deletions java/host-test/stub/android/content/Context.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* NNStreamer Android API host test
* Copyright (C) 2026 Samsung Electronics Co., Ltd.
*/

package android.content;

import android.content.res.AssetManager;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

/**
* Host stub of android.content.Context for running the instrumented tests on a desktop JVM.
* The files directory is a temporary directory, and the assets are empty.
*/
public class Context {
private final File mFilesDir;
private final AssetManager mAssets = new AssetManager();

public Context() {
try {
mFilesDir = Files.createTempDirectory("nns-host-test").toFile();
mFilesDir.deleteOnExit();
} catch (IOException e) {
throw new IllegalStateException("Failed to create the files directory", e);
}
}

public AssetManager getAssets() {
return mAssets;
}

public File getFilesDir() {
return mFilesDir;
}

public String getPackageName() {
return "org.nnsuite.nnstreamer.test";
}
}
24 changes: 24 additions & 0 deletions java/host-test/stub/android/content/res/AssetManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* NNStreamer Android API host test
* Copyright (C) 2026 Samsung Electronics Co., Ltd.
*/

package android.content.res;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
* Host stub of android.content.res.AssetManager. The host test has no assets.
*/
public final class AssetManager {
public String[] list(String path) throws IOException {
return null;
}

public InputStream open(String fileName) throws IOException {
throw new FileNotFoundException(fileName);
}
}
19 changes: 19 additions & 0 deletions java/host-test/stub/android/os/Build.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* NNStreamer Android API host test
* Copyright (C) 2026 Samsung Electronics Co., Ltd.
*/

package android.os;

/**
* Host stub of android.os.Build.
*/
public final class Build {
public static final String HARDWARE = "host";
public static final String MANUFACTURER = "host";

public static final class VERSION {
public static final int SDK_INT = 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* NNStreamer Android API host test
* Copyright (C) 2026 Samsung Electronics Co., Ltd.
*/

package android.support.test;

import android.content.Context;

/**
* Host stub of android.support.test.InstrumentationRegistry.
*/
public final class InstrumentationRegistry {
private static Context sContext;

public static synchronized Context getTargetContext() {
if (sContext == null) {
sContext = new Context();
}

return sContext;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* NNStreamer Android API host test
* Copyright (C) 2026 Samsung Electronics Co., Ltd.
*/

package android.support.test.runner;

import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;

/**
* Host stub of android.support.test.runner.AndroidJUnit4, the plain JUnit 4 runner.
*/
public final class AndroidJUnit4 extends BlockJUnit4ClassRunner {
public AndroidJUnit4(Class<?> klass) throws InitializationError {
super(klass);
}
}
Loading
Loading