blob: 399758479e1b69f05daea7d3abf1cc3309fbe741 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:test/test.dart';
import 'package:jni/jni.dart';
import '../test_util/callback_types.dart';
import 'c_based/dart_bindings/simple_package.dart';
const pi = 3.14159;
const fpDelta = 0.001;
const trillion = 1024 * 1024 * 1024 * 1024;
void _runJavaGC() {
final managementFactory =
Jni.findJClass('java/lang/management/ManagementFactory');
final bean = managementFactory.callStaticMethodByName<JObject>(
'getRuntimeMXBean', '()Ljava/lang/management/RuntimeMXBean;', []);
final pid = bean.callMethodByName<int>('getPid', '()J', []);
ProcessResult result;
do {
result = Process.runSync('jcmd', [pid.toString(), 'GC.run']);
sleep(const Duration(milliseconds: 100));
} while (result.exitCode != 0);
}
void registerTests(String groupName, TestRunnerCallback test) {
group(groupName, () {
test('static final fields - int', () {
expect(Example.ON, equals(1));
expect(Example.OFF, equals(0));
expect(Example.PI, closeTo(pi, fpDelta));
expect(Example.SEMICOLON, equals(';'.codeUnitAt(0)));
expect(Example.SEMICOLON_STRING.toDartString(releaseOriginal: true),
equals(';'));
});
test('Static methods - primitive', () {
// same test can be run at a replicated (dart-only) test, check for both
// possible values.
expect(Example.getAmount(), isIn([1012, 500]));
Example.setAmount(1012);
expect(Example.getAmount(), equals(1012));
expect(Example.getAsterisk(), equals('*'.codeUnitAt(0)));
expect(C2.CONSTANT, equals(12));
});
test('Static fields & methods - string', () {
expect(
Example.getName().toDartString(releaseOriginal: true),
isIn(["Ragnar Lothbrok", "Theseus"]),
);
Example.setName("Theseus".toJString());
expect(
Example.getName().toDartString(releaseOriginal: true),
equals("Theseus"),
);
});
test('Static fields and methods - Object', () {
final nested = Example.getNestedInstance();
expect(nested.getValue(), isIn([true, false]));
nested.setValue(false);
expect(nested.getValue(), isFalse);
});
test('static methods with several arguments', () {
expect(Example.addInts(10, 15), equals(25));
expect(Example.max4(-1, 15, 30, 12), equals(30));
expect(Example.max8(1, 4, 8, 2, 4, 10, 8, 6), equals(10));
});
test('Instance methods (getters & setters)', () {
final e = Example();
expect(e.getNumber(), equals(0));
expect(e.getIsUp(), true);
expect(e.getCodename().toDartString(), equals("achilles"));
e.setNumber(1);
e.setUp(false);
e.setCodename("spartan".toJString());
expect(e.getIsUp(), false);
expect(e.getNumber(), 1);
expect(e.getCodename().toDartString(), equals("spartan"));
e.release();
});
test('Instance methods with several arguments', () {
final e = Example();
expect(e.add4Longs(1, 2, 3, 4), equals(10));
expect(e.add8Longs(1, 1, 2, 2, 3, 3, 12, 24), equals(48));
expect(
e.add4Longs(trillion, trillion, trillion, trillion),
equals(4 * trillion),
);
expect(
e.add8Longs(trillion, -trillion, trillion, -trillion, trillion,
-trillion, -trillion, -trillion),
equals(2 * -trillion),
);
e.release();
});
test('Misc. instance methods', () {
final e = Example();
final rand = e.getRandom();
expect(rand.isNull, isFalse);
final _ = e.getRandomLong();
final id =
e.getRandomNumericString(rand).toDartString(releaseOriginal: true);
expect(int.parse(id), lessThan(10000));
e.setNumber(145);
expect(
e.getSelf().getSelf().getSelf().getSelf().getNumber(),
equals(145),
);
e.release();
});
test('Constructors', () {
final e0 = Example();
expect(e0.getNumber(), 0);
expect(e0.getIsUp(), true);
expect(e0.getCodename().toDartString(), equals('achilles'));
final e1 = Example.new1(111);
expect(e1.getNumber(), equals(111));
expect(e1.getIsUp(), true);
expect(e1.getCodename().toDartString(), "achilles");
final e2 = Example.new2(122, false);
expect(e2.getNumber(), equals(122));
expect(e2.getIsUp(), false);
expect(e2.getCodename().toDartString(), "achilles");
final e3 = Example.new3(133, false, "spartan".toJString());
expect(e3.getNumber(), equals(133));
expect(e3.getIsUp(), false);
expect(e3.getCodename().toDartString(), "spartan");
});
test('Static (non-final) fields', () {
// Other replica test may already have modified this, so assert both
// values.
expect(Fields.amount, isIn([500, 101]));
Fields.amount = 101;
expect(Fields.amount, equals(101));
expect(Fields.asterisk, equals('*'.codeUnitAt(0)));
expect(
Fields.name.toDartString(),
isIn(["Earl Haraldson", "Ragnar Lothbrok"]),
);
Fields.name = "Ragnar Lothbrok".toJString();
expect(Fields.name.toDartString(), equals("Ragnar Lothbrok"));
expect(Fields.pi, closeTo(pi, fpDelta));
});
test('Instance fields', () {
final f = Fields();
expect(f.trillion, equals(trillion));
expect(f.isAchillesDead, isFalse);
expect(f.bestFighterInGreece.toDartString(), equals("Achilles"));
// "For your glory walks hand-in-hand with your doom." - Thetis.
f.isAchillesDead = true;
// I don't know much Greek mythology. But Troy was released in 2004,
// and 300 was released in 2006, so it's Leonidas I.
f.bestFighterInGreece = "Leonidas I".toJString();
expect(f.isAchillesDead, isTrue);
expect(f.bestFighterInGreece.toDartString(), "Leonidas I");
});
test('Fields from nested class', () {
expect(Fields_Nested().hundred, equals(100));
// Hector of Troy may disagree.
expect(Fields_Nested.BEST_GOD.toDartString(), equals('Pallas Athena'));
});
test('static methods arrays', () {
final array = Example.getArr();
expect(array[0], 1);
expect(array[1], 2);
expect(array[2], 3);
expect(Example.addAll(array), 6);
array[0] = 4;
expect(Example.addAll(array), 9);
});
test('array of the class', () {
final ex1 = Example();
final ex2 = Example();
ex1.setNumber(1);
ex2.setNumber(2);
final array = JArray(Example.type, 2);
array[0] = ex1;
array[1] = ex2;
expect(array[0].getNumber(), 1);
expect(array[1].getNumber(), 2);
array.release();
ex1.release();
ex2.release();
});
test("Check bindings for same-named classes", () {
expect(Example().whichExample(), 0);
expect(Example1().whichExample(), 1);
});
test('Unicode char', () {
expect(Fields.euroSymbol, equals('\u20AC'.codeUnitAt(0)));
});
group('exception tests', () {
void throwsException(void Function() f) {
expect(f, throwsA(isA<JniException>()));
}
test('Example throw exception', () {
throwsException(Example.throwException);
});
test('Exception from method returning Object', () {
throwsException(Exceptions.staticObjectMethod);
throwsException(Exceptions.staticObjectArrayMethod);
final x = Exceptions();
throwsException(x.objectMethod);
throwsException(x.objectArrayMethod);
});
test('Exception from method returning int', () {
throwsException(Exceptions.staticIntMethod);
throwsException(Exceptions.staticIntArrayMethod);
final x = Exceptions();
throwsException(x.intMethod);
throwsException(x.intArrayMethod);
});
test('Exception from constructor', () {
throwsException(() => Exceptions.new1(6.8));
throwsException(() => Exceptions.new2(1, 2, 3, 4, 5, 6));
});
test('Exception contains error message & stack trace', () {
try {
Exceptions.throwLoremIpsum();
} on JniException catch (e) {
expect(e.message, stringContainsInOrder(["Lorem Ipsum"]));
expect(
e.toString(),
stringContainsInOrder(["Lorem Ipsum", "throwLoremIpsum"]),
);
return;
}
throw AssertionError("No exception was thrown");
});
});
group('generics', () {
test('GrandParent constructor', () {
using((arena) {
final grandParent =
GrandParent('Hello'.toJString()..releasedBy(arena))
..releasedBy(arena);
expect(grandParent, isA<GrandParent<JString>>());
expect(grandParent.$type, isA<$GrandParentType<JString>>());
expect(
grandParent.value.toDartString(releaseOriginal: true), 'Hello');
});
});
test('MyStack<T>', () {
using((arena) {
final stack = MyStack(T: JString.type)..releasedBy(arena);
stack.push('Hello'.toJString()..releasedBy(arena));
stack.push('World'.toJString()..releasedBy(arena));
expect(stack.pop().toDartString(releaseOriginal: true), 'World');
expect(stack.pop().toDartString(releaseOriginal: true), 'Hello');
});
});
test('Different stacks have different types, same stacks have same types',
() {
using((arena) {
final aStringStack = MyStack(T: JString.type)..releasedBy(arena);
final anotherStringStack = MyStack(T: JString.type)
..releasedBy(arena);
final anObjectStack = MyStack(T: JObject.type)..releasedBy(arena);
expect(aStringStack.$type, anotherStringStack.$type);
expect(
aStringStack.$type.hashCode,
anotherStringStack.$type.hashCode,
);
expect(aStringStack.$type, isNot(anObjectStack.$type));
expect(
aStringStack.$type.hashCode,
isNot(anObjectStack.$type.hashCode),
);
});
});
test('MyMap<K, V>', () {
using((arena) {
final map = MyMap(K: JString.type, V: Example.type)
..releasedBy(arena);
final helloExample = Example.new1(1)..releasedBy(arena);
final worldExample = Example.new1(2)..releasedBy(arena);
map.put('Hello'.toJString()..releasedBy(arena), helloExample);
map.put('World'.toJString()..releasedBy(arena), worldExample);
expect(
(map.get0('Hello'.toJString()..releasedBy(arena))
..releasedBy(arena))
.getNumber(),
1,
);
expect(
(map.get0('World'.toJString()..releasedBy(arena))
..releasedBy(arena))
.getNumber(),
2,
);
expect(
((map.entryStack()..releasedBy(arena)).pop()..releasedBy(arena))
.key
.castTo(JString.type, releaseOriginal: true)
.toDartString(releaseOriginal: true),
anyOf('Hello', 'World'),
);
});
});
group('classes extending generics', () {
test('StringStack', () {
using((arena) {
final stringStack = StringStack()..releasedBy(arena);
stringStack.push('Hello'.toJString()..releasedBy(arena));
expect(
stringStack.pop().toDartString(releaseOriginal: true), 'Hello');
});
});
test('StringKeyedMap', () {
using((arena) {
final map = StringKeyedMap(V: Example.type)..releasedBy(arena);
final example = Example()..releasedBy(arena);
map.put('Hello'.toJString()..releasedBy(arena), example);
expect(
(map.get0('Hello'.toJString()..releasedBy(arena))
..releasedBy(arena))
.getNumber(),
0,
);
});
});
test('StringValuedMap', () {
using((arena) {
final map = StringValuedMap(K: Example.type)..releasedBy(arena);
final example = Example()..releasedBy(arena);
map.put(example, 'Hello'.toJString()..releasedBy(arena));
expect(
map.get0(example).toDartString(releaseOriginal: true),
'Hello',
);
});
});
test('StringMap', () {
using((arena) {
final map = StringMap()..releasedBy(arena);
map.put('hello'.toJString()..releasedBy(arena),
'world'.toJString()..releasedBy(arena));
expect(
map
.get0('hello'.toJString()..releasedBy(arena))
.toDartString(releaseOriginal: true),
'world',
);
});
});
});
test('superclass count', () {
expect(JObject.type.superCount, 0);
expect(MyMap.type(JObject.type, JObject.type).superCount, 1);
expect(StringKeyedMap.type(JObject.type).superCount, 2);
expect(StringValuedMap.type(JObject.type).superCount, 2);
expect(StringMap.type.superCount, 3);
});
test('nested generics', () {
using((arena) {
final grandParent =
GrandParent(T: JString.type, "!".toJString()..releasedBy(arena))
..releasedBy(arena);
expect(
grandParent.value.toDartString(releaseOriginal: true),
"!",
);
final strStaticParent = GrandParent.stringStaticParent()
..releasedBy(arena);
expect(
strStaticParent.value.toDartString(releaseOriginal: true),
"Hello",
);
final exampleStaticParent = GrandParent.varStaticParent(
S: Example.type, Example()..releasedBy(arena))
..releasedBy(arena);
expect(
(exampleStaticParent.value..releasedBy(arena)).getNumber(),
0,
);
final strParent = grandParent.stringParent()..releasedBy(arena);
expect(
strParent.parentValue
.castTo(JString.type, releaseOriginal: true)
.toDartString(releaseOriginal: true),
"!",
);
expect(
strParent.value.toDartString(releaseOriginal: true),
"Hello",
);
final exampleParent = grandParent.varParent(
S: Example.type, Example()..releasedBy(arena))
..releasedBy(arena);
expect(
exampleParent.parentValue
.castTo(JString.type, releaseOriginal: true)
.toDartString(releaseOriginal: true),
"!",
);
expect(
(exampleParent.value..releasedBy(arena)).getNumber(),
0,
);
// TODO(#139): test constructing Child, currently does not work due
// to a problem with C-bindings.
});
});
});
test('Constructing non-static nested classes', () {
using((arena) {
final grandParent = GrandParent(1.toJInteger())..releasedBy(arena);
final parent = GrandParent_Parent(grandParent, 2.toJInteger())
..releasedBy(arena);
final child = GrandParent_Parent_Child(parent, 3.toJInteger())
..releasedBy(arena);
expect(grandParent.value.intValue(releaseOriginal: true), 1);
expect(parent.parentValue.intValue(releaseOriginal: true), 1);
expect(parent.value.intValue(releaseOriginal: true), 2);
expect(child.grandParentValue.intValue(releaseOriginal: true), 1);
expect(child.parentValue.intValue(releaseOriginal: true), 2);
expect(child.value.intValue(releaseOriginal: true), 3);
});
});
group('Generic type inference', () {
test('MyStack.of1', () {
using((arena) {
final emptyStack = MyStack(T: JString.type)..releasedBy(arena);
expect(emptyStack.size(), 0);
final stack = MyStack.of1(
"Hello".toJString()..releasedBy(arena),
)..releasedBy(arena);
expect(stack, isA<MyStack<JString>>());
expect(stack.$type, isA<$MyStackType<JString>>());
expect(
stack.pop().toDartString(releaseOriginal: true),
"Hello",
);
});
});
test('MyStack.of 2 strings', () {
using((arena) {
final stack = MyStack.of2(
"Hello".toJString()..releasedBy(arena),
"World".toJString()..releasedBy(arena),
)..releasedBy(arena);
expect(stack, isA<MyStack<JString>>());
expect(stack.$type, isA<$MyStackType<JString>>());
expect(
stack.pop().toDartString(releaseOriginal: true),
"World",
);
expect(
stack.pop().toDartString(releaseOriginal: true),
"Hello",
);
});
});
test('MyStack.of a string and an array', () {
using((arena) {
final array = JArray.filled(1, "World".toJString()..releasedBy(arena))
..releasedBy(arena);
final stack = MyStack.of2(
"Hello".toJString()..releasedBy(arena),
array,
)..releasedBy(arena);
expect(stack, isA<MyStack<JObject>>());
expect(stack.$type, isA<$MyStackType<JObject>>());
expect(
stack
.pop()
.castTo(JArray.type(JString.type), releaseOriginal: true)[0]
.toDartString(releaseOriginal: true),
"World",
);
expect(
stack
.pop()
.castTo(JString.type, releaseOriginal: true)
.toDartString(releaseOriginal: true),
"Hello",
);
});
});
test('MyStack.from array of string', () {
using((arena) {
final array = JArray.filled(1, "Hello".toJString()..releasedBy(arena))
..releasedBy(arena);
final stack = MyStack.fromArray(array)..releasedBy(arena);
expect(stack, isA<MyStack<JString>>());
expect(stack.$type, isA<$MyStackType<JString>>());
expect(
stack.pop().toDartString(releaseOriginal: true),
"Hello",
);
});
});
test('MyStack.fromArrayOfArrayOfGrandParents', () {
using((arena) {
final firstDimention = JArray.filled(
1,
GrandParent("Hello".toJString()..releasedBy(arena))
..releasedBy(arena),
)..releasedBy(arena);
final twoDimentionalArray = JArray.filled(1, firstDimention)
..releasedBy(arena);
final stack =
MyStack.fromArrayOfArrayOfGrandParents(twoDimentionalArray)
..releasedBy(arena);
expect(stack, isA<MyStack<JString>>());
expect(stack.$type, isA<$MyStackType<JString>>());
expect(
stack.pop().toDartString(releaseOriginal: true),
"Hello",
);
});
});
});
});
group('interface implementation', () {
for (final (threading, consume) in [
('another thread', MyInterfaceConsumer.consumeOnAnotherThread),
('the same thread', MyInterfaceConsumer.consumeOnSameThread),
]) {
test('MyInterface.implement on $threading', () async {
final voidCallbackResult = Completer<JString>();
final varCallbackResult = Completer<JInteger>();
final manyPrimitivesResult = Completer<int>();
// We can use this trick to access self, instead of generating a `thiz`
// or `self` argument for each one of the callbacks.
late final MyInterface<JInteger> myInterface;
myInterface = MyInterface.implement(
$MyInterfaceImpl(
voidCallback: (s) {
voidCallbackResult.complete(s);
},
stringCallback: (s) {
return (s.toDartString(releaseOriginal: true) * 2).toJString();
},
varCallback: (JInteger t) {
final result =
(t.intValue(releaseOriginal: true) * 2).toJInteger();
varCallbackResult.complete(result);
return result;
},
manyPrimitives: (a, b, c, d) {
if (b) {
final result = a + c + d.toInt();
manyPrimitivesResult.complete(result);
return result;
} else {
// Call self, add to [a] when [b] is false and change b to true.
return myInterface.manyPrimitives(a + 1, true, c, d);
}
},
T: JInteger.type,
),
);
// [stringCallback] is going to be called first using [s].
// The result of it is going to be used as the argument for
// [voidCallback].
// The other two methods will be called individually using the passed
// arguments afterwards.
consume(
myInterface,
// For stringCallback:
'hello'.toJString(),
// For manyPrimitives:
-1,
false,
3,
3.14,
// For varCallback
7.toJInteger(),
);
final voidCallback = await voidCallbackResult.future;
expect(voidCallback.toDartString(releaseOriginal: true), 'hellohello');
final varCallback = await varCallbackResult.future;
expect(varCallback.intValue(), 14);
final manyPrimitives = await manyPrimitivesResult.future;
expect(manyPrimitives, -1 + 3 + 3.14.toInt() + 1);
// Currently we have one implementation of the interface.
expect(MyInterface.$impls, hasLength(1));
myInterface.release();
// Running System.gc() and waiting.
_runJavaGC();
for (var i = 0; i < 8; ++i) {
await Future<void>.delayed(Duration(milliseconds: (1 << i) * 100));
if (MyInterface.$impls.isEmpty) {
break;
}
}
// Since the interface is now deleted, the cleaner must signal to Dart
// to clean up.
expect(MyInterface.$impls, isEmpty);
});
}
group('Dart exceptions are handled', () {
for (final exception in [UnimplementedError(), 'Hello!']) {
for (final sameThread in [true, false]) {
test(
'on ${sameThread ? 'the same thread' : 'another thread'}'
' throwing $exception', () async {
final runnable = MyRunnable.implement(
$MyRunnableImpl(
run: () {
throw exception;
},
),
);
final runner = MyRunnableRunner(runnable);
if (sameThread) {
runner.runOnSameThread();
} else {
runner.runOnAnotherThread();
}
while (runner.error.isNull) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
expect(
Jni.env.IsInstanceOf(
runner.error.reference,
Jni.findClass('java/lang/reflect/UndeclaredThrowableException'),
),
isTrue,
);
final cause = runner.error.callMethodByName<JObject>(
'getCause', '()Ljava/lang/Throwable;', []);
expect(
Jni.env.IsInstanceOf(
cause.reference,
Jni.findClass(
'com/github/dart_lang/jni/PortProxy\$DartException'),
),
isTrue,
);
expect(cause.toString(), contains(exception.toString()));
});
}
}
});
});
group('$groupName (load tests)', () {
const k4 = 4 * 1024; // This is a round number, unlike say 4000
const k256 = 256 * 1024;
test('Create large number of JNI references without deleting', () {
for (int i = 0; i < k4; i++) {
final e = Example.new1(i);
expect(e.getNumber(), equals(i));
}
});
test('Create many JNI refs with scoped deletion', () {
for (int i = 0; i < k256; i++) {
using((arena) {
final e = Example.new1(i)..releasedBy(arena);
expect(e.getNumber(), equals(i));
});
}
});
test('Create many JNI refs with scoped deletion, in batches', () {
for (int i = 0; i < 256; i++) {
using((arena) {
for (int i = 0; i < 1024; i++) {
final e = Example.new1(i)..releasedBy(arena);
expect(e.getNumber(), equals(i));
}
});
}
});
test('Create large number of JNI refs with manual delete', () {
for (int i = 0; i < k256; i++) {
final e = Example.new1(i);
expect(e.getNumber(), equals(i));
e.release();
}
});
test('Method returning primitive type does not create references', () {
using((arena) {
final e = Example.new1(64)..releasedBy(arena);
for (int i = 0; i < k256; i++) {
expect(e.getNumber(), equals(64));
}
});
});
test('Class references are cached', () {
final asterisk = '*'.codeUnitAt(0);
for (int i = 0; i < k256; i++) {
expect(Fields.asterisk, equals(asterisk));
}
});
void testPassageOfTime(int n) {
test('Refs are not inadvertently deleted after $n seconds', () {
final f = Fields();
expect(f.trillion, equals(trillion));
sleep(Duration(seconds: n));
expect(f.trillion, equals(trillion));
});
}
if (!Platform.isAndroid) {
testPassageOfTime(1);
testPassageOfTime(4);
}
});
}