diff --git a/tools/chatgui-android/tests/jni_utf8/EventSink.java b/tools/chatgui-android/tests/jni_utf8/EventSink.java new file mode 100644 index 00000000..a56f6cc4 --- /dev/null +++ b/tools/chatgui-android/tests/jni_utf8/EventSink.java @@ -0,0 +1,13 @@ +/** Настоящий Java callback для проверки регистрации, замены и исключений доставки JNI. */ +public final class EventSink { + public int calls; + public int type; + public int bytes; + public boolean fail; + public void onEvent(int eventType, byte[] data) { + calls++; + type = eventType; + bytes = data == null ? 0 : data.length; + if (fail) throw new IllegalStateException("injected event callback failure"); + } +} diff --git a/tools/chatgui-android/tests/jni_utf8/run.py b/tools/chatgui-android/tests/jni_utf8/run.py index cfb65265..c3d6fef7 100644 --- a/tools/chatgui-android/tests/jni_utf8/run.py +++ b/tools/chatgui-android/tests/jni_utf8/run.py @@ -25,12 +25,13 @@ def main(): root / "src/chat", root / "src/media_async", root / "lib"] with tempfile.TemporaryDirectory(prefix="utun-jni-utf8-") as output: binary = Path(output) / "test_jni_utf8" + subprocess.run([str(java_home / "bin/javac"), "-d", output, str(directory / "EventSink.java")], check=True, timeout=30) command = shlex.split(os.environ.get("CC", "cc")) + ["-std=c99", "-D_DEFAULT_SOURCE", "-D__ANDROID__", "-DUSE_OPENSSL", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-pthread", "-Wno-incompatible-pointer-types"] command += [f"-I{path}" for path in includes] command += [str(directory / "test_jni_utf8.c"), f"-L{server}", f"-Wl,-rpath,{server}", "-ljvm", "-o", str(binary)] subprocess.run(command, check=True, timeout=90) - subprocess.run([str(binary)], check=True, timeout=30) + subprocess.run([str(binary)], cwd=output, check=True, timeout=30) if __name__ == "__main__": diff --git a/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c b/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c index b40d7233..c56c7c98 100644 --- a/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c +++ b/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c @@ -34,10 +34,37 @@ int main(void) { CHECK(!(*env)->ThrowNew(env, exception, "preserved")); CHECK(!jni_utf8(env, original) && !jni_text(env, expected) && (*env)->ExceptionCheck(env)); (*env)->ExceptionClear(env); + g_jvm = vm; + jclass sink_class = (*env)->FindClass(env, "EventSink"); CHECK(sink_class); + jmethodID constructor = (*env)->GetMethodID(env, sink_class, "", "()V"); CHECK(constructor); + jobject sink = (*env)->NewObject(env, sink_class, constructor); CHECK(!(*env)->ExceptionCheck(env) && sink); + jfieldID calls = (*env)->GetFieldID(env, sink_class, "calls", "I"); CHECK(calls); + jfieldID type = (*env)->GetFieldID(env, sink_class, "type", "I"); CHECK(type); + jfieldID size = (*env)->GetFieldID(env, sink_class, "bytes", "I"); CHECK(size); + jfieldID fail = (*env)->GetFieldID(env, sink_class, "fail", "Z"); CHECK(fail); + Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(env, NULL, sink); CHECK(!(*env)->ExceptionCheck(env)); + const uint8_t payload[] = {1, 2, 3}; + jni_on_event(7, payload, sizeof(payload)); + CHECK((*env)->GetIntField(env, sink, calls) == 1 && (*env)->GetIntField(env, sink, type) == 7); + CHECK((*env)->GetIntField(env, sink, size) == sizeof(payload)); + (*env)->SetBooleanField(env, sink, fail, JNI_TRUE); + jni_on_event(8, payload, sizeof(payload)); CHECK(!(*env)->ExceptionCheck(env)); + (*env)->SetBooleanField(env, sink, fail, JNI_FALSE); + jni_on_event(9, NULL, 0); CHECK((*env)->GetIntField(env, sink, calls) == 3 && !(*env)->GetIntField(env, sink, size)); + jobject replacement = (*env)->NewObject(env, sink_class, constructor); CHECK(!(*env)->ExceptionCheck(env) && replacement); + Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(env, NULL, replacement); CHECK(!(*env)->ExceptionCheck(env)); + jni_on_event(10, payload, sizeof(payload)); + CHECK((*env)->GetIntField(env, replacement, calls) == 1 && (*env)->GetIntField(env, sink, calls) == 3); + CHECK(!(*env)->ThrowNew(env, exception, "preserved event exception")); + jni_on_event(11, payload, sizeof(payload)); CHECK((*env)->ExceptionCheck(env)); (*env)->ExceptionClear(env); + jni_on_event(12, NULL, 3); CHECK((*env)->GetIntField(env, replacement, calls) == 1); + Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(env, NULL, NULL); CHECK(!(*env)->ExceptionCheck(env)); + jni_on_event(13, NULL, 0); CHECK(!(*env)->ExceptionCheck(env)); + (*env)->DeleteLocalRef(env, replacement); (*env)->DeleteLocalRef(env, sink); (*env)->DeleteLocalRef(env, sink_class); jstring empty = jni_text(env, ""); CHECK(empty && !(*env)->GetStringLength(env, empty)); (*env)->DeleteLocalRef(env, empty); (*env)->DeleteLocalRef(env, exception); (*env)->DeleteLocalRef(env, embedded); (*env)->DeleteLocalRef(env, original); CHECK(!(*env)->ExceptionCheck(env) && (*vm)->DestroyJavaVM(vm) == JNI_OK); - puts("JNI UTF-8: 1024 real JVM roundtrips, allocation failure/retry, embedded NUL and pending exception PASS"); + puts("JNI UTF-8/events: real JVM roundtrips, allocation failure/retry, NUL, callback replacement and exceptions PASS"); return 0; }