Skip to content

Commit e200a70

Browse files
authored
fix(jni): dart to native type conversion (#3372)
* Update * Update * Fix test * Update * Update * Update * Update * Add JNI utility tests for Dart to Java object conversion This commit introduces a new test file for verifying the conversion of Dart objects to JNI types, including primitives, lists, and maps. The tests ensure that null values are dropped appropriately and that nested structures are handled correctly. The tests are designed to run on the Android platform only. * Rename native FFI JNI utility test file to native JNI utility test for clarity * Refactor JNI utility tests to improve clarity and consistency This commit updates the JNI utility tests by renaming assertion functions for better readability and consistency. The changes include replacing direct assertions with dedicated helper functions that check for equality, ensuring null checks are performed, and enhancing the overall structure of the test cases. This refactor aims to improve maintainability and clarity in the test suite. * Remove null checks from setContexts and setExtra methods in SentryNativeJava class for cleaner code. * Refactor JNI utility tests to enhance structure and readability This commit updates the JNI utility tests by restructuring the test cases for better clarity and maintainability. Key changes include the introduction of local variables for input data, improved assertion handling with arena management, and the renaming of helper functions for consistency. These modifications aim to streamline the testing process and ensure accurate validation of Dart to JNI object conversions. * Enhance JNI utility tests by adding missing line breaks for improved readability This commit introduces line breaks in the JNI utility test file to enhance the overall readability of the code. The changes aim to improve the visual structure of the test cases, making it easier to follow the logic and organization of the tests.
1 parent 4ff8a1b commit e200a70

File tree

7 files changed

+311
-27
lines changed

7 files changed

+311
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Dart to native type conversion ([#3372](https://github.com/getsentry/sentry-dart/pull/3372))
78
- Revert FFI usage on iOS/macOS due to symbol stripping issues ([#3379](https://github.com/getsentry/sentry-dart/pull/3379))
89

910
### Dependencies

packages/flutter/example/integration_test/all.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import 'integration_test.dart' as a;
33
import 'profiling_test.dart' as b;
44
import 'replay_test.dart' as c;
55
import 'platform_integrations_test.dart' as d;
6+
import 'native_jni_utils_test.dart' as e;
67

78
void main() {
89
a.main();
910
b.main();
1011
c.main();
1112
d.main();
13+
e.main();
1214
}

packages/flutter/example/integration_test/integration_test.dart

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -707,11 +707,20 @@ void main() {
707707
});
708708

709709
// 1. Add a breadcrumb via Dart
710+
final customObject = CustomObject();
710711
final testBreadcrumb = Breadcrumb(
711-
message: 'test-breadcrumb-message',
712-
category: 'test-category',
713-
level: SentryLevel.info,
714-
);
712+
message: 'test-breadcrumb-message',
713+
category: 'test-category',
714+
level: SentryLevel.info,
715+
data: {
716+
'string': 'data',
717+
'int': 12,
718+
'bool': true,
719+
'double': 12.34,
720+
'map': {'nested': 'data', 'custom object': customObject},
721+
'list': [1, customObject, 3],
722+
'custom object': customObject
723+
});
715724
await Sentry.addBreadcrumb(testBreadcrumb);
716725

717726
// 2. Verify it appears in native via loadContexts
@@ -732,6 +741,17 @@ void main() {
732741
expect(testCrumb, isNotNull,
733742
reason: 'Test breadcrumb should exist in native breadcrumbs');
734743
expect(testCrumb['category'], equals('test-category'));
744+
expect(testCrumb['level'], equals('info'));
745+
expect(testCrumb['data'], isNotNull);
746+
expect(testCrumb['data']['map'], isNotNull);
747+
expect(testCrumb['data']['map']['nested'], equals('data'));
748+
expect(testCrumb['data']['map']['custom object'],
749+
equals(customObject.toString()));
750+
expect(testCrumb['data']['list'], isNotNull);
751+
expect(testCrumb['data']['list'][0], equals(1));
752+
expect(testCrumb['data']['list'][1], equals(customObject.toString()));
753+
expect(testCrumb['data']['list'][2], equals(3));
754+
expect(testCrumb['data']['custom object'], equals(customObject.toString()));
735755

736756
// 3. Clear breadcrumbs
737757
await Sentry.configureScope((scope) async {
@@ -751,10 +771,20 @@ void main() {
751771
});
752772

753773
// 1. Set a user via Dart
774+
final customObject = CustomObject();
754775
final testUser = SentryUser(
755776
id: 'test-user-id',
756777
757778
username: 'test-username',
779+
data: {
780+
'string': 'data',
781+
'int': 12,
782+
'bool': true,
783+
'double': 12.34,
784+
'map': {'nested': 'data', 'custom object': customObject},
785+
'list': [1, customObject, 3],
786+
'custom object': customObject
787+
},
758788
);
759789
await Sentry.configureScope((scope) async {
760790
await scope.setUser(testUser);
@@ -769,6 +799,26 @@ void main() {
769799
expect(user!['id'], equals('test-user-id'));
770800
expect(user['email'], equals('[email protected]'));
771801
expect(user['username'], equals('test-username'));
802+
expect(user['data']['map'], isNotNull);
803+
expect(user['data']['list'], isNotNull);
804+
expect(user['data']['custom object'], equals(customObject.toString()));
805+
806+
if (Platform.isAndroid) {
807+
// On Android, the Java SDK's User.data field only supports Map<String, String>.
808+
// Nested Maps and Lists are converted to Java's HashMap/ArrayList toString()
809+
// format (e.g., {key=value} instead of {"key":"value"}).
810+
expect(user['data']['map'],
811+
equals('{nested=data, custom object=${customObject.toString()}}'));
812+
expect(
813+
user['data']['list'], equals('[1, ${customObject.toString()}, 3]'));
814+
} else {
815+
expect(user['data']['map']['nested'], equals('data'));
816+
expect(user['data']['map']['custom object'],
817+
equals(customObject.toString()));
818+
expect(user['data']['list'][0], equals(1));
819+
expect(user['data']['list'][1], equals(customObject.toString()));
820+
expect(user['data']['list'][2], equals(3));
821+
}
772822

773823
// 3. Clear user (after clearing the id should remain)
774824
await Sentry.configureScope((scope) async {
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// ignore_for_file: depend_on_referenced_packages
2+
@TestOn('vm')
3+
4+
import 'dart:io';
5+
6+
import 'package:test/test.dart';
7+
import 'package:jni/jni.dart';
8+
import 'package:sentry_flutter/src/native/java/sentry_native_java.dart';
9+
10+
import 'utils.dart';
11+
12+
void main() {
13+
final customObject = CustomObject();
14+
15+
final inputNestedMap = {
16+
'innerString': 'nested',
17+
'innerList': [1, null, 2],
18+
'innerNull': null,
19+
};
20+
21+
final inputList = [
22+
'value',
23+
1,
24+
1.1,
25+
true,
26+
customObject,
27+
['nestedList', 2],
28+
inputNestedMap,
29+
null,
30+
];
31+
32+
final inputMap = {
33+
'key': 'value',
34+
'key2': 1,
35+
'key3': 1.1,
36+
'key4': true,
37+
'key5': customObject,
38+
'list': inputList,
39+
'nestedMap': inputNestedMap,
40+
'nullEntry': null,
41+
};
42+
43+
final expectedNestedList = ['nestedList', 2];
44+
45+
final expectedNestedMap = {
46+
'innerString': 'nested',
47+
'innerList': [1, 2],
48+
'innerNull': null,
49+
};
50+
final expectedList = [
51+
'value',
52+
1,
53+
1.1,
54+
true,
55+
customObject.toString(),
56+
expectedNestedList,
57+
expectedNestedMap,
58+
];
59+
60+
final expectedMap = {
61+
'key': 'value',
62+
'key2': 1,
63+
'key3': 1.1,
64+
'key4': true,
65+
'key5': customObject.toString(),
66+
'list': expectedList,
67+
'nestedMap': expectedNestedMap,
68+
};
69+
70+
group('JNI (Android)', () {
71+
test('dartToJObject converts primitives', () {
72+
using((arena) {
73+
_expectJniStringEquals(
74+
dartToJObject('value')..releasedBy(arena), 'value');
75+
_expectJniLongEquals(dartToJObject(1)..releasedBy(arena), 1);
76+
_expectJniDoubleEquals(dartToJObject(1.1)..releasedBy(arena), 1.1);
77+
_expectJniBoolEquals(dartToJObject(true)..releasedBy(arena), true);
78+
_expectJniStringEquals(
79+
dartToJObject(customObject)..releasedBy(arena),
80+
customObject.toString(),
81+
);
82+
});
83+
});
84+
85+
test('dartToJObject converts list (drops nulls)', () {
86+
using((arena) {
87+
final javaList = dartToJObject(inputList).as(JList.type(JObject.type))
88+
..releasedBy(arena);
89+
_expectJniList(javaList, expectedList, arena);
90+
});
91+
});
92+
93+
test('dartToJObject converts map (drops null values)', () {
94+
using((arena) {
95+
final javaMap = dartToJObject(inputMap)
96+
.as(JMap.type(JString.type, JObject.type))
97+
..releasedBy(arena);
98+
_expectJniMap(javaMap, expectedMap, arena);
99+
});
100+
});
101+
102+
test('dartToJList', () {
103+
using((arena) {
104+
final javaList = dartToJList(inputList)..releasedBy(arena);
105+
_expectJniList(javaList, expectedList, arena);
106+
});
107+
});
108+
109+
test('dartToJMap', () {
110+
using((arena) {
111+
final javaMap = dartToJMap(inputMap)..releasedBy(arena);
112+
_expectJniMap(javaMap, expectedMap, arena);
113+
});
114+
});
115+
}, skip: !Platform.isAndroid);
116+
}
117+
118+
void _expectJniStringEquals(JObject? javaObject, String expected) {
119+
expect(javaObject, isNotNull);
120+
final javaString = javaObject!.as(JString.type);
121+
expect(javaString.toDartString(releaseOriginal: true), expected);
122+
}
123+
124+
void _expectJniLongEquals(JObject? javaObject, int expected) {
125+
expect(javaObject, isNotNull);
126+
final javaLong = javaObject!.as(JLong.type);
127+
expect(javaLong.longValue(releaseOriginal: true), expected);
128+
}
129+
130+
void _expectJniDoubleEquals(JObject? javaObject, double expected) {
131+
expect(javaObject, isNotNull);
132+
final javaDouble = javaObject!.as(JDouble.type);
133+
expect(javaDouble.doubleValue(releaseOriginal: true), expected);
134+
}
135+
136+
void _expectJniBoolEquals(JObject? javaObject, bool expected) {
137+
expect(javaObject, isNotNull);
138+
final javaBoolean = javaObject!.as(JBoolean.type);
139+
expect(javaBoolean.booleanValue(releaseOriginal: true), expected);
140+
}
141+
142+
JObject? _get(JMap<JString, JObject> javaMap, String key, Arena arena) =>
143+
javaMap[key.toJString()..releasedBy(arena)];
144+
145+
void _expectJniList(
146+
JList<JObject> javaList,
147+
List<Object?> expectedListValues,
148+
Arena arena,
149+
) {
150+
expect(javaList.length, expectedListValues.length);
151+
152+
_expectJniStringEquals(javaList[0], expectedListValues[0] as String);
153+
_expectJniLongEquals(javaList[1], expectedListValues[1] as int);
154+
_expectJniDoubleEquals(javaList[2], expectedListValues[2] as double);
155+
_expectJniBoolEquals(javaList[3], expectedListValues[3] as bool);
156+
_expectJniStringEquals(javaList[4], expectedListValues[4] as String);
157+
158+
final nestedList = javaList[5].as(JList.type(JObject.type))
159+
..releasedBy(arena);
160+
final expectedNestedList = expectedListValues[5] as List<Object?>;
161+
expect(nestedList.length, expectedNestedList.length);
162+
_expectJniStringEquals(nestedList[0], expectedNestedList[0] as String);
163+
_expectJniLongEquals(nestedList[1], expectedNestedList[1] as int);
164+
165+
final nestedMap = javaList[6].as(JMap.type(JString.type, JObject.type))
166+
..releasedBy(arena);
167+
_expectJniNestedMap(
168+
nestedMap,
169+
expectedListValues[6] as Map<String, Object?>,
170+
expectedNestedList.length,
171+
arena,
172+
);
173+
}
174+
175+
void _expectJniMap(
176+
JMap<JString, JObject> javaMap,
177+
Map<String, Object?> expectedMapValues,
178+
Arena arena,
179+
) {
180+
expect(javaMap.length, expectedMapValues.length);
181+
182+
final expectedList = expectedMapValues['list']! as List<Object?>;
183+
final expectedNestedList = expectedList[5] as List<Object?>;
184+
final expectedNestedMap =
185+
expectedMapValues['nestedMap']! as Map<String, Object?>;
186+
187+
_expectJniStringEquals(
188+
_get(javaMap, 'key', arena), expectedMapValues['key'] as String);
189+
_expectJniLongEquals(
190+
_get(javaMap, 'key2', arena), expectedMapValues['key2'] as int);
191+
_expectJniDoubleEquals(
192+
_get(javaMap, 'key3', arena), expectedMapValues['key3'] as double);
193+
_expectJniBoolEquals(
194+
_get(javaMap, 'key4', arena), expectedMapValues['key4'] as bool);
195+
_expectJniStringEquals(
196+
_get(javaMap, 'key5', arena), expectedMapValues['key5'] as String);
197+
198+
final nestedList = _get(javaMap, 'list', arena)!.as(JList.type(JObject.type))
199+
..releasedBy(arena);
200+
_expectJniList(nestedList, expectedList, arena);
201+
202+
final nestedMap = _get(javaMap, 'nestedMap', arena)!
203+
.as(JMap.type(JString.type, JObject.type))
204+
..releasedBy(arena);
205+
_expectJniNestedMap(
206+
nestedMap, expectedNestedMap, expectedNestedList.length, arena);
207+
208+
expect(_get(javaMap, 'nullEntry', arena), isNull);
209+
}
210+
211+
void _expectJniNestedMap(
212+
JMap<JString, JObject> javaNestedMap,
213+
Map<String, Object?> expectedNestedMapValues,
214+
int expectedNestedListLength,
215+
Arena arena,
216+
) {
217+
_expectJniStringEquals(_get(javaNestedMap, 'innerString', arena),
218+
expectedNestedMapValues['innerString'] as String);
219+
220+
final innerList = _get(javaNestedMap, 'innerList', arena)!
221+
.as(JList.type(JObject.type))
222+
..releasedBy(arena);
223+
expect(innerList.length, expectedNestedListLength);
224+
_expectJniLongEquals(innerList[0],
225+
(expectedNestedMapValues['innerList']! as List<Object?>)[0] as int);
226+
_expectJniLongEquals(innerList[1],
227+
(expectedNestedMapValues['innerList']! as List<Object?>)[1] as int);
228+
229+
expect(_get(javaNestedMap, 'innerNull', arena), isNull);
230+
}

packages/flutter/example/integration_test/utils.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ FutureOr<void> restoreFlutterOnErrorAfter(FutureOr<void> Function() fn) async {
2222
}
2323

2424
const fakeDsn = 'https://[email protected]/1234567';
25+
26+
// Used to test for correct serialization of custom object in attributes / data.
27+
class CustomObject {}

0 commit comments

Comments
 (0)