diff --git a/pkgs/jni/lib/src/jfinal_string.dart b/pkgs/jni/lib/src/jfinal_string.dart deleted file mode 100644 index ce3279981..000000000 --- a/pkgs/jni/lib/src/jfinal_string.dart +++ /dev/null @@ -1,31 +0,0 @@ -// 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:ffi'; - -import 'jreference.dart'; -import 'lang/jstring.dart'; -import 'third_party/generated_bindings.dart'; - -/// Used for `static final` Java strings, where the constant string is -/// available. -/// -/// If only its value is used using [toDartString], the [reference] is never -/// populated, saving a method call. -class JFinalString extends JString with JLazyReference { - @override - final JObjectPtr Function() lazyReference; - - final String string; - - JFinalString(this.lazyReference, this.string) : super.fromRef(nullptr); - - @override - String toDartString({bool releaseOriginal = false}) { - if (releaseOriginal) { - release(); - } - return string; - } -} diff --git a/pkgs/jni/lib/src/jreference.dart b/pkgs/jni/lib/src/jreference.dart index 093e297df..8d8b71dcd 100644 --- a/pkgs/jni/lib/src/jreference.dart +++ b/pkgs/jni/lib/src/jreference.dart @@ -29,9 +29,8 @@ extension ProtectedJReference on JReference { /// /// Detaches the finalizer so the underlying pointer will not be deleted. JObjectPtr toPointer() { - final ref = reference; setAsReleased(); - return ref; + return _reference; } } @@ -43,9 +42,7 @@ abstract class JReference implements Finalizable { NativeFinalizer(Jni.env.ptr.ref.DeleteGlobalRef.cast()); JReference.fromRef(this._reference) { - if (_reference != nullptr) { - _finalizer.attach(this, _reference, detach: this); - } + _finalizer.attach(this, _reference, detach: this); } bool _released = false; @@ -83,45 +80,6 @@ abstract class JReference implements Finalizable { void releasedBy(Arena arena) => arena.onReleaseAll(release); } -/// Creates a "lazy" [JReference]. -/// -/// The first use of [reference] will call [lazyReference]. -/// -/// This is useful when the Java object is not necessarily used directly, and -/// there are alternative ways to get a Dart representation of the Object. -/// -/// Object mixed in with this must call their super.[fromRef] constructor -/// with [nullptr]. -/// -/// Also see [JFinalString]. -mixin JLazyReference on JReference { - JObjectPtr? _lazyReference; - - JObjectPtr Function() get lazyReference; - - @override - JObjectPtr get reference { - if (_lazyReference == null) { - _lazyReference = lazyReference(); - JReference._finalizer.attach(this, _lazyReference!, detach: this); - return _lazyReference!; - } - if (_released) { - throw UseAfterReleaseError(); - } - return _lazyReference!; - } - - @override - void release() { - setAsReleased(); - if (_lazyReference == null) { - return; - } - Jni.env.DeleteGlobalRef(_lazyReference!); - } -} - extension JReferenceUseExtension on T { /// Applies [callback] on [this] object and then delete the underlying JNI /// reference, returning the result of [callback]. diff --git a/pkgs/jni/test/jfinal_string_test.dart b/pkgs/jni/test/jfinal_string_test.dart deleted file mode 100644 index c8a998001..000000000 --- a/pkgs/jni/test/jfinal_string_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -// 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:io'; - -import 'package:jni/jni.dart'; -import 'package:jni/src/jfinal_string.dart'; -import 'package:test/test.dart'; - -import 'test_util/test_util.dart'; - -void main() { - // Don't forget to initialize JNI. - if (!Platform.isAndroid) { - checkDylibIsUpToDate(); - Jni.spawnIfNotExists(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]); - } - run(testRunner: test); -} - -void run({required TestRunnerCallback testRunner}) { - testRunner('JFinalString', () { - const string = 'abc'; - var referenceFetchedCount = 0; - final finalString = JFinalString( - () { - ++referenceFetchedCount; - return string.toJString().reference; - }, - string, - ); - expect(finalString.toDartString(), string); - expect(referenceFetchedCount, 0); - expect(finalString.reference, isNot(nullptr)); - expect(referenceFetchedCount, 1); - finalString.reference; - expect(referenceFetchedCount, 1); - expect(finalString.toDartString(releaseOriginal: true), string); - expect(() => finalString.reference, throwsA(isA())); - expect(finalString.release, throwsA(isA())); - }); -} diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index e57349385..9a7e8dbb1 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -2,6 +2,9 @@ - **Breaking Change**: The generated impl class for interfaces is now an `interface`. +- **Breaking Change** ([#792](https://github.com/dart-lang/native/issues/792)]): + `static final String` fields get converted to `JString` getters instead of + `static const String` fields in Dart. ## 0.7.0 diff --git a/pkgs/jnigen/example/in_app_java/lib/android_utils.dart b/pkgs/jnigen/example/in_app_java/lib/android_utils.dart index 6e677a901..6f6340d11 100644 --- a/pkgs/jnigen/example/in_app_java/lib/android_utils.dart +++ b/pkgs/jnigen/example/in_app_java/lib/android_utils.dart @@ -140,8 +140,13 @@ class EmojiCompat extends jni.JObject { /// The type which includes information such as the signature of this class. static const type = $EmojiCompatType(); + static final _get_EDITOR_INFO_METAVERSION_KEY = + jniLookup>( + "get_EmojiCompat__EDITOR_INFO_METAVERSION_KEY") + .asFunction(); /// from: static public final java.lang.String EDITOR_INFO_METAVERSION_KEY + /// The returned object must be released after use, by calling the [release] method. /// /// Key in EditorInfo\#extras that represents the emoji metadata version used by the /// widget. The existence of the value means that the widget is using EmojiCompat. @@ -149,15 +154,21 @@ class EmojiCompat extends jni.JObject { /// If exists, the value for the key is an {@code int} and can be used to query EmojiCompat to /// see whether the widget has the ability to display a certain emoji using /// \#hasEmojiGlyph(CharSequence, int). - static const EDITOR_INFO_METAVERSION_KEY = - r"""android.support.text.emoji.emojiCompat_metadataVersion"""; + static jni.JString get EDITOR_INFO_METAVERSION_KEY => const jni.JStringType() + .fromRef(_get_EDITOR_INFO_METAVERSION_KEY().object); + + static final _get_EDITOR_INFO_REPLACE_ALL_KEY = + jniLookup>( + "get_EmojiCompat__EDITOR_INFO_REPLACE_ALL_KEY") + .asFunction(); /// from: static public final java.lang.String EDITOR_INFO_REPLACE_ALL_KEY + /// The returned object must be released after use, by calling the [release] method. /// /// Key in EditorInfo\#extras that represents EmojiCompat.Config\#setReplaceAll(boolean) configuration parameter. The key is added only if /// EmojiCompat is used by the widget. If exists, the value is a boolean. - static const EDITOR_INFO_REPLACE_ALL_KEY = - r"""android.support.text.emoji.emojiCompat_replaceAll"""; + static jni.JString get EDITOR_INFO_REPLACE_ALL_KEY => const jni.JStringType() + .fromRef(_get_EDITOR_INFO_REPLACE_ALL_KEY().object); /// from: static public final int LOAD_STATE_DEFAULT /// @@ -323,7 +334,6 @@ class EmojiCompat extends jni.JObject { /// androidx.core.graphics.PaintCompat\#hasGlyph(Paint, String) for each emoji /// subsequence. static const EMOJI_FALLBACK = 2; - static final _init = jniLookup< ffi .NativeFunction)>>( @@ -2398,9 +2408,15 @@ class Build_Partition extends jni.JObject { /// The type which includes information such as the signature of this class. static const type = $Build_PartitionType(); + static final _get_PARTITION_NAME_SYSTEM = + jniLookup>( + "get_Build_Partition__PARTITION_NAME_SYSTEM") + .asFunction(); /// from: static public final java.lang.String PARTITION_NAME_SYSTEM - static const PARTITION_NAME_SYSTEM = r"""system"""; + /// The returned object must be released after use, by calling the [release] method. + static jni.JString get PARTITION_NAME_SYSTEM => + const jni.JStringType().fromRef(_get_PARTITION_NAME_SYSTEM().object); static final _getName = jniLookup< ffi @@ -2754,7 +2770,6 @@ class Build_VERSION_CODES extends jni.JObject { /// from: static public final int TIRAMISU static const TIRAMISU = 33; - static final _new0 = jniLookup>( "Build_VERSION_CODES__new0") .asFunction(); @@ -3058,8 +3073,15 @@ class Build extends jni.JObject { static jni.JString get TYPE => const jni.JStringType().fromRef(_get_TYPE().object); + static final _get_UNKNOWN = + jniLookup>( + "get_Build__UNKNOWN") + .asFunction(); + /// from: static public final java.lang.String UNKNOWN - static const UNKNOWN = r"""unknown"""; + /// The returned object must be released after use, by calling the [release] method. + static jni.JString get UNKNOWN => + const jni.JStringType().fromRef(_get_UNKNOWN().object); static final _get_USER = jniLookup>("get_Build__USER") diff --git a/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c b/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c index 983838bc9..381224d4c 100644 --- a/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c +++ b/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c @@ -517,6 +517,36 @@ JniResult EmojiCompat__updateEditorInfo(jobject self_, jobject outAttrs) { return (JniResult){.value = {.j = 0}, .exception = check_exception()}; } +jfieldID _f_EmojiCompat__EDITOR_INFO_METAVERSION_KEY = NULL; +FFI_PLUGIN_EXPORT +JniResult get_EmojiCompat__EDITOR_INFO_METAVERSION_KEY() { + load_env(); + load_class_global_ref(&_c_EmojiCompat, "androidx/emoji2/text/EmojiCompat"); + if (_c_EmojiCompat == NULL) + return (JniResult){.value = {.j = 0}, .exception = check_exception()}; + load_static_field(_c_EmojiCompat, + &_f_EmojiCompat__EDITOR_INFO_METAVERSION_KEY, + "EDITOR_INFO_METAVERSION_KEY", "Ljava/lang/String;"); + jobject _result = (*jniEnv)->GetStaticObjectField( + jniEnv, _c_EmojiCompat, _f_EmojiCompat__EDITOR_INFO_METAVERSION_KEY); + return to_global_ref_result(_result); +} + +jfieldID _f_EmojiCompat__EDITOR_INFO_REPLACE_ALL_KEY = NULL; +FFI_PLUGIN_EXPORT +JniResult get_EmojiCompat__EDITOR_INFO_REPLACE_ALL_KEY() { + load_env(); + load_class_global_ref(&_c_EmojiCompat, "androidx/emoji2/text/EmojiCompat"); + if (_c_EmojiCompat == NULL) + return (JniResult){.value = {.j = 0}, .exception = check_exception()}; + load_static_field(_c_EmojiCompat, + &_f_EmojiCompat__EDITOR_INFO_REPLACE_ALL_KEY, + "EDITOR_INFO_REPLACE_ALL_KEY", "Ljava/lang/String;"); + jobject _result = (*jniEnv)->GetStaticObjectField( + jniEnv, _c_EmojiCompat, _f_EmojiCompat__EDITOR_INFO_REPLACE_ALL_KEY); + return to_global_ref_result(_result); +} + // androidx.emoji2.text.EmojiCompat$Config jclass _c_EmojiCompat_Config = NULL; @@ -1435,6 +1465,21 @@ JniResult Build_Partition__hashCode1(jobject self_) { return (JniResult){.value = {.i = _result}, .exception = check_exception()}; } +jfieldID _f_Build_Partition__PARTITION_NAME_SYSTEM = NULL; +FFI_PLUGIN_EXPORT +JniResult get_Build_Partition__PARTITION_NAME_SYSTEM() { + load_env(); + load_class_global_ref(&_c_Build_Partition, "android/os/Build$Partition"); + if (_c_Build_Partition == NULL) + return (JniResult){.value = {.j = 0}, .exception = check_exception()}; + load_static_field(_c_Build_Partition, + &_f_Build_Partition__PARTITION_NAME_SYSTEM, + "PARTITION_NAME_SYSTEM", "Ljava/lang/String;"); + jobject _result = (*jniEnv)->GetStaticObjectField( + jniEnv, _c_Build_Partition, _f_Build_Partition__PARTITION_NAME_SYSTEM); + return to_global_ref_result(_result); +} + // android.os.Build$VERSION jclass _c_Build_VERSION = NULL; @@ -2048,6 +2093,20 @@ JniResult get_Build__TYPE() { return to_global_ref_result(_result); } +jfieldID _f_Build__UNKNOWN = NULL; +FFI_PLUGIN_EXPORT +JniResult get_Build__UNKNOWN() { + load_env(); + load_class_global_ref(&_c_Build, "android/os/Build"); + if (_c_Build == NULL) + return (JniResult){.value = {.j = 0}, .exception = check_exception()}; + load_static_field(_c_Build, &_f_Build__UNKNOWN, "UNKNOWN", + "Ljava/lang/String;"); + jobject _result = + (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__UNKNOWN); + return to_global_ref_result(_result); +} + jfieldID _f_Build__USER = NULL; FFI_PLUGIN_EXPORT JniResult get_Build__USER() { diff --git a/pkgs/jnigen/lib/src/bindings/c_generator.dart b/pkgs/jnigen/lib/src/bindings/c_generator.dart index 79b07c8b6..b99e7136a 100644 --- a/pkgs/jnigen/lib/src/bindings/c_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/c_generator.dart @@ -251,7 +251,10 @@ class _CFieldGenerator extends Visitor { // If the field is final and default is assigned, then no need to wrap // this field. It should then be a constant in dart code. - if (node.isStatic && node.isFinal && node.defaultValue != null) { + if (node.isStatic && + node.isFinal && + node.defaultValue != null && + (node.defaultValue is num || node.defaultValue is bool)) { return; } diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index d01621cc0..fb76f15aa 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -1071,16 +1071,9 @@ class _FieldGenerator extends Visitor { if (node.isFinal && node.isStatic && node.defaultValue != null) { final name = node.finalName; final value = node.defaultValue!; - // TODO(#31): Should we leave String as a normal getter instead? - if (value is String || value is num || value is bool) { + if (value is num || value is bool) { writeDocs(node, writeReleaseInstructions: false); - s.write(' static const $name = '); - if (value is String) { - s.write('r"""$value"""'); - } else { - s.write(value); - } - s.writeln(';\n'); + s.writeln(' static const $name = $value;'); return; } } diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/jackson_core.c b/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/jackson_core.c index 3203612ea..11bc5da43 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/jackson_core.c +++ b/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/jackson_core.c @@ -1386,6 +1386,21 @@ JniResult JsonFactory__createJsonGenerator2(jobject self_, jobject out) { return to_global_ref_result(_result); } +jfieldID _f_JsonFactory__FORMAT_NAME_JSON = NULL; +FFI_PLUGIN_EXPORT +JniResult get_JsonFactory__FORMAT_NAME_JSON() { + load_env(); + load_class_global_ref(&_c_JsonFactory, + "com/fasterxml/jackson/core/JsonFactory"); + if (_c_JsonFactory == NULL) + return (JniResult){.value = {.j = 0}, .exception = check_exception()}; + load_static_field(_c_JsonFactory, &_f_JsonFactory__FORMAT_NAME_JSON, + "FORMAT_NAME_JSON", "Ljava/lang/String;"); + jobject _result = (*jniEnv)->GetStaticObjectField( + jniEnv, _c_JsonFactory, _f_JsonFactory__FORMAT_NAME_JSON); + return to_global_ref_result(_result); +} + jfieldID _f_JsonFactory__DEFAULT_FACTORY_FEATURE_FLAGS = NULL; FFI_PLUGIN_EXPORT JniResult get_JsonFactory__DEFAULT_FACTORY_FEATURE_FLAGS() { diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/c_based/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart b/pkgs/jnigen/test/jackson_core_test/third_party/c_based/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart index 822a05261..beb1dd5ae 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/c_based/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/c_based/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart @@ -71,12 +71,18 @@ class JsonFactory extends jni.JObject { /// The type which includes information such as the signature of this class. static const type = $JsonFactoryType(); + static final _get_FORMAT_NAME_JSON = + jniLookup>( + "get_JsonFactory__FORMAT_NAME_JSON") + .asFunction(); /// from: static public final java.lang.String FORMAT_NAME_JSON + /// The returned object must be released after use, by calling the [release] method. /// /// Name used to identify JSON format /// (and returned by \#getFormatName() - static const FORMAT_NAME_JSON = r"""JSON"""; + static jni.JString get FORMAT_NAME_JSON => + const jni.JStringType().fromRef(_get_FORMAT_NAME_JSON().object); static final _get_DEFAULT_FACTORY_FEATURE_FLAGS = jniLookup>( diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/dart_only/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart b/pkgs/jnigen/test/jackson_core_test/third_party/dart_only/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart index 5aab2d45b..aa0264e1a 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/dart_only/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/dart_only/dart_bindings/com/fasterxml/jackson/core/JsonFactory.dart @@ -73,12 +73,22 @@ class JsonFactory extends jni.JObject { /// The type which includes information such as the signature of this class. static const type = $JsonFactoryType(); + static final _id_FORMAT_NAME_JSON = jni.Jni.accessors.getStaticFieldIDOf( + _class.reference, + r"FORMAT_NAME_JSON", + r"Ljava/lang/String;", + ); /// from: static public final java.lang.String FORMAT_NAME_JSON + /// The returned object must be released after use, by calling the [release] method. /// /// Name used to identify JSON format /// (and returned by \#getFormatName() - static const FORMAT_NAME_JSON = r"""JSON"""; + static jni.JString get FORMAT_NAME_JSON => const jni.JStringType().fromRef(jni + .Jni.accessors + .getStaticField( + _class.reference, _id_FORMAT_NAME_JSON, jni.JniCallType.objectType) + .object); static final _id_DEFAULT_FACTORY_FEATURE_FLAGS = jni.Jni.accessors.getStaticFieldIDOf( diff --git a/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c b/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c index 20c7e7e53..f1fee4e2a 100644 --- a/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c +++ b/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c @@ -793,6 +793,21 @@ JniResult Example__overloaded4(jobject self_, jobject a) { return (JniResult){.value = {.j = 0}, .exception = check_exception()}; } +jfieldID _f_Example__SEMICOLON_STRING = NULL; +FFI_PLUGIN_EXPORT +JniResult get_Example__SEMICOLON_STRING() { + load_env(); + load_class_global_ref(&_c_Example, + "com/github/dart_lang/jnigen/simple_package/Example"); + if (_c_Example == NULL) + return (JniResult){.value = {.j = 0}, .exception = check_exception()}; + load_static_field(_c_Example, &_f_Example__SEMICOLON_STRING, + "SEMICOLON_STRING", "Ljava/lang/String;"); + jobject _result = (*jniEnv)->GetStaticObjectField( + jniEnv, _c_Example, _f_Example__SEMICOLON_STRING); + return to_global_ref_result(_result); +} + jfieldID _f_Example__unusedRandom = NULL; FFI_PLUGIN_EXPORT JniResult get_Example__unusedRandom() { diff --git a/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart index 8149d4e98..cf8c6456a 100644 --- a/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart @@ -113,9 +113,15 @@ class Example extends jni.JObject { /// from: static public final char SEMICOLON static const SEMICOLON = 59; + static final _get_SEMICOLON_STRING = + jniLookup>( + "get_Example__SEMICOLON_STRING") + .asFunction(); /// from: static public final java.lang.String SEMICOLON_STRING - static const SEMICOLON_STRING = r""";"""; + /// The returned object must be released after use, by calling the [release] method. + static jni.JString get SEMICOLON_STRING => + const jni.JStringType().fromRef(_get_SEMICOLON_STRING().object); static final _get_unusedRandom = jniLookup>( diff --git a/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart index 8e0f1ce86..cfb8fcf79 100644 --- a/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart @@ -119,9 +119,19 @@ class Example extends jni.JObject { /// from: static public final char SEMICOLON static const SEMICOLON = 59; + static final _id_SEMICOLON_STRING = jni.Jni.accessors.getStaticFieldIDOf( + _class.reference, + r"SEMICOLON_STRING", + r"Ljava/lang/String;", + ); /// from: static public final java.lang.String SEMICOLON_STRING - static const SEMICOLON_STRING = r""";"""; + /// The returned object must be released after use, by calling the [release] method. + static jni.JString get SEMICOLON_STRING => const jni.JStringType().fromRef(jni + .Jni.accessors + .getStaticField( + _class.reference, _id_SEMICOLON_STRING, jni.JniCallType.objectType) + .object); static final _id_unusedRandom = jni.Jni.accessors.getStaticFieldIDOf( _class.reference, diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index 3791f9e45..399758479 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -36,7 +36,8 @@ void registerTests(String groupName, TestRunnerCallback test) { expect(Example.OFF, equals(0)); expect(Example.PI, closeTo(pi, fpDelta)); expect(Example.SEMICOLON, equals(';'.codeUnitAt(0))); - expect(Example.SEMICOLON_STRING, equals(';')); + expect(Example.SEMICOLON_STRING.toDartString(releaseOriginal: true), + equals(';')); }); test('Static methods - primitive', () {