[jnigen] Bunch of small testsuite improvements (https://github.com/dart-lang/jnigen/issues/257)
* Throw error in test if summarizer is stale
* Fail if jni dylib is stale
* Add spawnIfNotExists. (It sounds like SQL, which means no further evolution is possible)
* Add tags mechanism to jnigen test
* Add `summarizer_test` and `large_test` tags. So that in dev env, can
exclude these with -x tag for fast iteration.
* Adds dart_test.yaml which is required for this configuration by test
package.
* Use random seed for jnigen tests in CI
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index a5bb09e..1809e2c 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -92,8 +92,10 @@
- name: Run summarizer tests
run: mvn surefire:test
working-directory: ./pkgs/jnigen/java
+ - name: Build summarizer
+ run: dart run jnigen:setup
- name: Run VM tests
- run: dart test --platform vm
+ run: dart test --test-randomize-ordering-seed random
- name: Install coverage
run: dart pub global activate coverage
- name: Collect coverage
@@ -243,7 +245,10 @@
- name: build notification_plugin example APK
run: flutter build apk --target-platform=android-arm64
working-directory: ./pkgs/jnigen/example/notification_plugin/example
- - run: dart test
+ - name: Build summarizer
+ run: dart run jnigen:setup
+ - name: Run tests
+ run: dart test --test-randomize-ordering-seed random
test_jni_macos_minimal:
needs: [analyze_jni]
@@ -290,7 +295,7 @@
- run: git config --global core.autocrlf true
- run: dart pub get
- run: dart run jnigen:setup
- - run: dart test
+ - run: dart test --test-randomize-ordering-seed random
build_jni_example_linux:
runs-on: ubuntu-latest
diff --git a/pkgs/jni/bin/setup.dart b/pkgs/jni/bin/setup.dart
index eb4f9c9..3f5bb79 100644
--- a/pkgs/jni/bin/setup.dart
+++ b/pkgs/jni/bin/setup.dart
@@ -7,12 +7,13 @@
import 'package:args/args.dart';
import 'package:package_config/package_config.dart';
-const ansiRed = '\x1b[31m';
-const ansiDefault = '\x1b[39;49m';
+import 'package:jni/src/build_util/build_util.dart';
const jniNativeBuildDirective =
'# jni_native_build (Build with jni:setup. Do not delete this line.)';
+// When changing this constant here, also change corresponding path in
+// test/test_util.
const _defaultRelativeBuildPath = "build/jni_libs";
const _buildPath = "build-path";
@@ -126,19 +127,6 @@
return sources;
}
-/// Returns true if [artifact] does not exist, or any file in [sourceDir] is
-/// newer than [artifact].
-bool needsBuild(File artifact, Directory sourceDir) {
- if (!artifact.existsSync()) return true;
- final fileLastModified = artifact.lastModifiedSync();
- for (final entry in sourceDir.listSync(recursive: true)) {
- if (entry.statSync().modified.isAfter(fileLastModified)) {
- return true;
- }
- }
- return false;
-}
-
/// Returns the name of file built using sources in [cDir]
String getTargetName(Directory cDir) {
for (final file in cDir.listSync(recursive: true)) {
diff --git a/pkgs/jni/lib/src/build_util/build_util.dart b/pkgs/jni/lib/src/build_util/build_util.dart
new file mode 100644
index 0000000..8d267df
--- /dev/null
+++ b/pkgs/jni/lib/src/build_util/build_util.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2022, 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.
+
+// Any shared build logic should be here. This way it can be reused across bin/,
+// tool/ and test/.
+
+import 'dart:io';
+
+const ansiRed = '\x1b[31m';
+const ansiDefault = '\x1b[39;49m';
+
+/// Returns true if [artifact] does not exist, or any file in [sourceDir] is
+/// newer than [artifact].
+bool needsBuild(File artifact, Directory sourceDir) {
+ if (!artifact.existsSync()) return true;
+ final fileLastModified = artifact.lastModifiedSync();
+ for (final entry in sourceDir.listSync(recursive: true)) {
+ if (entry.statSync().modified.isAfter(fileLastModified)) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart
index 277c2a5..97ff46e 100644
--- a/pkgs/jni/lib/src/jni.dart
+++ b/pkgs/jni/lib/src/jni.dart
@@ -85,13 +85,33 @@
List<String> classPath = const [],
bool ignoreUnrecognized = false,
int jniVersion = JniVersions.JNI_VERSION_1_6,
+ }) {
+ final status = spawnIfNotExists(
+ dylibDir: dylibDir,
+ jvmOptions: jvmOptions,
+ classPath: classPath,
+ ignoreUnrecognized: ignoreUnrecognized,
+ jniVersion: jniVersion,
+ );
+ if (status == false) {
+ throw JvmExistsException();
+ }
+ }
+
+ /// Same as [spawn] but if a JVM exists, returns silently instead of
+ /// throwing [JvmExistsException].
+ ///
+ /// If the options are different than that of existing VM, the existing VM's
+ /// options will remain in effect.
+ static bool spawnIfNotExists({
+ String? dylibDir,
+ List<String> jvmOptions = const [],
+ List<String> classPath = const [],
+ bool ignoreUnrecognized = false,
+ int jniVersion = JniVersions.JNI_VERSION_1_6,
}) =>
using((arena) {
_dylibDir = dylibDir;
- final existVm = _bindings.GetJavaVM();
- if (existVm != nullptr) {
- throw JvmExistsException();
- }
final jvmArgs = _createVMArgs(
options: jvmOptions,
classPath: classPath,
@@ -102,12 +122,9 @@
);
final status = _bindings.SpawnJvm(jvmArgs);
if (status == JniErrorCode.JNI_OK) {
- return;
+ return true;
} else if (status == DART_JNI_SINGLETON_EXISTS) {
- throw JvmExistsException();
- } else if (status == JniErrorCode.JNI_EEXIST) {
- sleep(const Duration(seconds: 1));
- throw JvmExistsException();
+ return false;
} else {
throw SpawnException.of(status);
}
diff --git a/pkgs/jni/test/exception_test.dart b/pkgs/jni/test/exception_test.dart
index 4fb34ef..01acfdf 100644
--- a/pkgs/jni/test/exception_test.dart
+++ b/pkgs/jni/test/exception_test.dart
@@ -11,6 +11,7 @@
void main() {
if (!Platform.isAndroid) {
+ checkDylibIsUpToDate();
bool caught = false;
try {
// If library does not exist, a helpful exception should be thrown.
diff --git a/pkgs/jni/test/global_env_test.dart b/pkgs/jni/test/global_env_test.dart
index 8f65d34..9b6a8b2 100644
--- a/pkgs/jni/test/global_env_test.dart
+++ b/pkgs/jni/test/global_env_test.dart
@@ -25,11 +25,8 @@
// You have to manually pass the path to the `dartjni` dynamic library.
if (!Platform.isAndroid) {
- try {
- Jni.spawn(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
- } on JvmExistsException catch (_) {
- // TODO(#51): Support destroying and reinstantiating JVM.
- }
+ checkDylibIsUpToDate();
+ Jni.spawnIfNotExists(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
}
run(testRunner: test);
}
diff --git a/pkgs/jni/test/jarray_test.dart b/pkgs/jni/test/jarray_test.dart
index 4eca3d4..5717d53 100644
--- a/pkgs/jni/test/jarray_test.dart
+++ b/pkgs/jni/test/jarray_test.dart
@@ -12,11 +12,8 @@
void main() {
// Don't forget to initialize JNI.
if (!Platform.isAndroid) {
- try {
- Jni.spawn(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
- } on JvmExistsException catch (_) {
- // TODO(#51): Support destroying and reinstantiating JVM.
- }
+ checkDylibIsUpToDate();
+ Jni.spawnIfNotExists(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
}
run(testRunner: test);
}
diff --git a/pkgs/jni/test/jobject_test.dart b/pkgs/jni/test/jobject_test.dart
index e951002..8478d5a 100644
--- a/pkgs/jni/test/jobject_test.dart
+++ b/pkgs/jni/test/jobject_test.dart
@@ -17,11 +17,8 @@
void main() {
// Don't forget to initialize JNI.
if (!Platform.isAndroid) {
- try {
- Jni.spawn(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
- } on JvmExistsException catch (_) {
- // TODO(#51): Support destroying and reinstantiating JVM.
- }
+ checkDylibIsUpToDate();
+ Jni.spawnIfNotExists(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
}
run(testRunner: test);
}
diff --git a/pkgs/jni/test/test_util/test_util.dart b/pkgs/jni/test/test_util/test_util.dart
index 40d86eb..07e711a 100644
--- a/pkgs/jni/test/test_util/test_util.dart
+++ b/pkgs/jni/test/test_util/test_util.dart
@@ -2,8 +2,37 @@
// 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/src/build_util/build_util.dart';
+
typedef TestCaseCallback = void Function();
typedef TestRunnerCallback = void Function(
String description,
TestCaseCallback test,
);
+
+final currentDir = Directory.current.uri;
+final dllSuffix =
+ Platform.isWindows ? "dll" : (Platform.isMacOS ? "dylib" : "so");
+final dllPrefix = Platform.isWindows ? '' : 'lib';
+final dllPath =
+ currentDir.resolve("build/jni_libs/${dllPrefix}dartjni.$dllSuffix");
+final srcPath = currentDir.resolve("src/");
+
+/// Fail if dartjni dll is stale.
+void checkDylibIsUpToDate() {
+ final dllFile = File.fromUri(dllPath);
+ if (needsBuild(File.fromUri(dllPath), Directory.fromUri(srcPath))) {
+ final cause = dllFile.existsSync()
+ ? 'not up-to-date with source modifications'
+ : 'not built';
+ var message = '\nFatal: dartjni.$dllSuffix is $cause. Please run '
+ '`dart run jni:setup` and try again.';
+ if (stderr.supportsAnsiEscapes) {
+ message = ansiRed + message + ansiDefault;
+ }
+ stderr.writeln(message);
+ exit(1);
+ }
+}
diff --git a/pkgs/jni/test/type_test.dart b/pkgs/jni/test/type_test.dart
index ecdcf10..68693a5 100644
--- a/pkgs/jni/test/type_test.dart
+++ b/pkgs/jni/test/type_test.dart
@@ -205,11 +205,8 @@
void main() {
if (!Platform.isAndroid) {
- try {
- Jni.spawn(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
- } on JvmExistsException catch (_) {
- // TODO(#51): Support destroying and reinstantiating JVM.
- }
+ checkDylibIsUpToDate();
+ Jni.spawnIfNotExists(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
}
run(testRunner: test);
}
diff --git a/pkgs/jnigen/dart_test.yaml b/pkgs/jnigen/dart_test.yaml
new file mode 100644
index 0000000..0fb13bc
--- /dev/null
+++ b/pkgs/jnigen/dart_test.yaml
@@ -0,0 +1,12 @@
+## 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.
+
+## This file defines test groups for jnigen. This will be helpful to exclude
+## lengthy tests by using -x flag.
+
+tags:
+ large_test:
+ timeout: 2x
+ summarizer_test:
+ timeout: 1x
diff --git a/pkgs/jnigen/test/bindings_test.dart b/pkgs/jnigen/test/bindings_test.dart
index 107c24d..7d98863 100644
--- a/pkgs/jnigen/test/bindings_test.dart
+++ b/pkgs/jnigen/test/bindings_test.dart
@@ -97,6 +97,7 @@
}
void main() async {
+ await checkLocallyBuiltDependencies();
setUpAll(setupDylibsAndClasses);
test('static final fields', () {
diff --git a/pkgs/jnigen/test/config_test.dart b/pkgs/jnigen/test/config_test.dart
index fd935c3..2faa528 100644
--- a/pkgs/jnigen/test/config_test.dart
+++ b/pkgs/jnigen/test/config_test.dart
@@ -10,6 +10,7 @@
import 'package:path/path.dart' as path show equals;
import 'jackson_core_test/generate.dart';
+import 'test_util/test_util.dart';
const packageTests = 'test';
final jacksonCoreTests = absolute(packageTests, 'jackson_core_test');
@@ -93,7 +94,8 @@
});
}
-void main() {
+void main() async {
+ await checkLocallyBuiltDependencies();
final config = Config.parseArgs([
'--config',
jnigenYaml,
diff --git a/pkgs/jnigen/test/dart_generator_test.dart b/pkgs/jnigen/test/dart_generator_test.dart
index 4ad2eb8..e9bbb76 100644
--- a/pkgs/jnigen/test/dart_generator_test.dart
+++ b/pkgs/jnigen/test/dart_generator_test.dart
@@ -5,7 +5,11 @@
import 'package:jnigen/src/bindings/dart_generator.dart';
import 'package:test/test.dart';
-void main() {
+import 'test_util/test_util.dart';
+
+void main() async {
+ await checkLocallyBuiltDependencies();
+
test('OutsideInBuffer', () {
final buffer = OutsideInBuffer();
buffer.appendLeft('f(');
diff --git a/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart b/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
index 75dde7d..9129612 100644
--- a/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
+++ b/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
@@ -11,6 +11,8 @@
import 'generate.dart';
void main() async {
+ await checkLocallyBuiltDependencies();
+
test("compare generated bindings for jackson_core", () async {
final lib = join(thirdPartyDir, 'lib');
final src = join(thirdPartyDir, 'src');
@@ -22,14 +24,16 @@
'not just required classes', () async {
final config = getConfig(generateFullVersion: true);
await generateAndAnalyzeBindings(config);
- }, timeout: const Timeout(Duration(minutes: 2)));
+ }, timeout: const Timeout(Duration(minutes: 2)), tags: largeTestTag);
+
test('generate and analyze bindings using ASM', () async {
final config = getConfig(generateFullVersion: true, useAsm: true);
await generateAndAnalyzeBindings(config);
- }, timeout: const Timeout(Duration(minutes: 2)));
+ }, timeout: const Timeout(Duration(minutes: 2)), tags: largeTestTag);
+
test('Generate and analyze pure dart bindings', () async {
final config = getConfig(generateFullVersion: true);
config.outputConfig.bindingsType = BindingsType.dartOnly;
await generateAndAnalyzeBindings(config);
- }, timeout: const Timeout.factor(2));
+ }, timeout: const Timeout.factor(2), tags: largeTestTag);
}
diff --git a/pkgs/jnigen/test/kotlin_test/generated_files_test.dart b/pkgs/jnigen/test/kotlin_test/generated_files_test.dart
index 9ae50e3..be9ff90 100644
--- a/pkgs/jnigen/test/kotlin_test/generated_files_test.dart
+++ b/pkgs/jnigen/test/kotlin_test/generated_files_test.dart
@@ -10,6 +10,9 @@
import '../test_util/test_util.dart';
void main() async {
+ // This is not run in setupAll, because we want to exit with one line of
+ // error message, not throw a long exception.
+ await checkLocallyBuiltDependencies();
test(
"Generate and compare bindings for kotlin_test",
() async {
@@ -29,5 +32,6 @@
);
},
timeout: const Timeout.factor(1.5),
- ); // test if generated file == expected file
+ tags: largeTestTag,
+ );
}
diff --git a/pkgs/jnigen/test/package_resolver_test.dart b/pkgs/jnigen/test/package_resolver_test.dart
index 39b8998..98c0d2b 100644
--- a/pkgs/jnigen/test/package_resolver_test.dart
+++ b/pkgs/jnigen/test/package_resolver_test.dart
@@ -5,6 +5,8 @@
import 'package:jnigen/src/bindings/resolver.dart';
import 'package:test/test.dart';
+import 'test_util/test_util.dart';
+
class ResolverTest {
ResolverTest(this.binaryName, this.expectedImport, this.expectedName);
String binaryName;
@@ -12,7 +14,8 @@
String expectedName;
}
-void main() {
+void main() async {
+ await checkLocallyBuiltDependencies();
final resolver = Resolver(
importMap: {
'org.apache.pdfbox': 'package:pdfbox/pdfbox.dart',
diff --git a/pkgs/jnigen/test/regenerate_examples_test.dart b/pkgs/jnigen/test/regenerate_examples_test.dart
index 0e2ac62..50e41b6 100644
--- a/pkgs/jnigen/test/regenerate_examples_test.dart
+++ b/pkgs/jnigen/test/regenerate_examples_test.dart
@@ -23,42 +23,59 @@
/// them to provided reference outputs.
///
/// [dartOutput] and [cOutput] are relative paths from example project dir.
-void testExample(String exampleName, String dartOutput, String? cOutput) {
- test('Generate and compare bindings for $exampleName',
- timeout: const Timeout.factor(2), () async {
- final examplePath = join('example', exampleName);
- final configPath = join(examplePath, 'jnigen.yaml');
+///
+/// Pass [isLargeTest] as true if the test will take considerable time.
+void testExample(String exampleName, String dartOutput, String? cOutput,
+ {bool isLargeTest = false}) {
+ test(
+ 'Generate and compare bindings for $exampleName',
+ timeout: const Timeout.factor(2),
+ () async {
+ final examplePath = join('example', exampleName);
+ final configPath = join(examplePath, 'jnigen.yaml');
- final dartBindingsPath = join(examplePath, dartOutput);
- String? cBindingsPath;
- if (cOutput != null) {
- cBindingsPath = join(examplePath, cOutput);
- }
+ final dartBindingsPath = join(examplePath, dartOutput);
+ String? cBindingsPath;
+ if (cOutput != null) {
+ cBindingsPath = join(examplePath, cOutput);
+ }
- final config = Config.parseArgs(['--config', configPath]);
- try {
- await generateAndCompareBindings(config, dartBindingsPath, cBindingsPath);
- } on GradleException catch (_) {
- stderr.writeln('Skip: $exampleName');
- }
- });
+ final config = Config.parseArgs(['--config', configPath]);
+ try {
+ await generateAndCompareBindings(
+ config, dartBindingsPath, cBindingsPath);
+ } on GradleException catch (_) {
+ stderr.writeln('Skip: $exampleName');
+ }
+ },
+ tags: isLargeTest ? largeTestTag : null,
+ );
}
-void main() {
+void main() async {
+ await checkLocallyBuiltDependencies();
testExample(
'in_app_java',
join('lib', 'android_utils.dart'),
join('src', 'android_utils'),
+ isLargeTest: true,
);
- testExample('pdfbox_plugin', join('lib', 'src', 'third_party'), 'src');
+ testExample(
+ 'pdfbox_plugin',
+ join('lib', 'src', 'third_party'),
+ 'src',
+ isLargeTest: false,
+ );
testExample(
'notification_plugin',
join('lib', 'notifications.dart'),
'src',
+ isLargeTest: true,
);
testExample(
'kotlin_plugin',
join('lib', 'kotlin_bindings.dart'),
'src',
+ isLargeTest: true,
);
}
diff --git a/pkgs/jnigen/test/simple_package_test/generated_files_test.dart b/pkgs/jnigen/test/simple_package_test/generated_files_test.dart
index 98a6d5f..084afe0 100644
--- a/pkgs/jnigen/test/simple_package_test/generated_files_test.dart
+++ b/pkgs/jnigen/test/simple_package_test/generated_files_test.dart
@@ -10,6 +10,8 @@
import '../test_util/test_util.dart';
void main() async {
+ await checkLocallyBuiltDependencies();
+
test("Generate and compare bindings for simple_package", () async {
await generateAndCompareBindings(
getConfig(),
diff --git a/pkgs/jnigen/test/summary_generation_test.dart b/pkgs/jnigen/test/summary_generation_test.dart
index 8386db6..e3582c6 100644
--- a/pkgs/jnigen/test/summary_generation_test.dart
+++ b/pkgs/jnigen/test/summary_generation_test.dart
@@ -5,6 +5,8 @@
// These tests validate summary generation in various scenarios.
// Currently, no validation of the summary content itself is done.
+@Tags(['summarizer_test'])
+
import 'dart:io';
import 'package:jnigen/src/config/config.dart';
@@ -107,9 +109,10 @@
);
}
-void main() {
+void main() async {
+ await checkLocallyBuiltDependencies();
late Directory tempDir;
- setUpAll(() {
+ setUpAll(() async {
tempDir = getTempDir("jnigen_summary_tests_");
});
diff --git a/pkgs/jnigen/test/test_util/test_util.dart b/pkgs/jnigen/test/test_util/test_util.dart
index 6870701..450e28a 100644
--- a/pkgs/jnigen/test/test_util/test_util.dart
+++ b/pkgs/jnigen/test/test_util/test_util.dart
@@ -5,14 +5,20 @@
import 'dart:io';
import 'package:jnigen/jnigen.dart';
+import 'package:jnigen/src/util/find_package.dart';
import 'package:path/path.dart' hide equals;
import 'package:test/test.dart';
import 'package:logging/logging.dart' show Level;
-import 'package:jnigen/src/logging/logging.dart' show printError, log;
+import 'package:jnigen/src/logging/logging.dart';
final _currentDirectory = Directory(".");
+// If changing these constants, grep for these values. In some places, test
+// package expects string literals.
+const largeTestTag = 'large_test';
+const summarizerTestTag = 'summarizer_test';
+
Directory getTempDir(String prefix) {
return _currentDirectory.createTempSync(prefix);
}
@@ -127,3 +133,28 @@
tempDir.deleteSync(recursive: true);
}
}
+
+final summarizerJar = join('.', '.dart_tool', 'jnigen', 'ApiSummarizer.jar');
+
+Future<void> failIfSummarizerNotBuilt() async {
+ final jarExists = await File(summarizerJar).exists();
+ if (!jarExists) {
+ stderr.writeln();
+ log.fatal('Please build summarizer by running '
+ '`dart run jnigen:setup` and try again');
+ }
+ final isJarStale = jarExists &&
+ await isPackageModifiedAfter(
+ 'jnigen', await File(summarizerJar).lastModified(), 'java/');
+ if (isJarStale) {
+ stderr.writeln();
+ log.fatal('Summarizer is not rebuilt after recent changes. '
+ 'Please run `dart run jnigen:setup` and try again.');
+ }
+}
+
+/// Verifies if locally built dependencies (currently `ApiSummarizer`)
+/// are up-to-date.
+Future<void> checkLocallyBuiltDependencies() async {
+ await failIfSummarizerNotBuilt();
+}