blob: cb74c7f9a49d4ece2eb46208d721f0fb89f38773 [file] [log] [blame]
// 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.
import 'dart:io';
import 'package:jnigen/jnigen.dart';
import 'package:jnigen/src/logging/logging.dart';
import 'package:jnigen/src/util/find_package.dart';
import 'package:logging/logging.dart' show Level;
import 'package:path/path.dart' hide equals;
import 'package:test/test.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);
}
Future<bool> isEmptyOrNotExistDir(String path) async {
final dir = Directory(path);
return (!await dir.exists()) || (await dir.list().length == 0);
}
/// Runs command, and prints output only if the exit status is non-zero.
Future<int> runCommandReturningStatus(String exec, List<String> args,
{String? workingDirectory, bool runInShell = false}) async {
final proc = await Process.run(exec, args,
workingDirectory: workingDirectory, runInShell: runInShell);
if (proc.exitCode != 0) {
printError('command exited with exit status ${proc.exitCode}:\n'
'$exec ${args.join(" ")}\n');
printError(proc.stdout);
printError(proc.stderr);
}
return proc.exitCode;
}
Future<void> runCommand(
String exec,
List<String> args, {
String? workingDirectory,
bool runInShell = false,
String? messageOnFailure,
}) async {
final status = await runCommandReturningStatus(
exec,
args,
workingDirectory: workingDirectory,
runInShell: runInShell,
);
if (status != 0) {
final message = messageOnFailure ?? 'Failed to execute $exec';
throw Exception('$message: Command exited with return code $status');
}
}
/// List all JAR files in [testRoot]/jar
Future<List<String>> getJarPaths(String testRoot) async {
final jarPath = join(testRoot, 'jar');
if (!await Directory(jarPath).exists()) {
return [];
}
return Directory(jarPath)
.list()
.map((entry) => entry.path)
.where((path) => path.endsWith('.jar'))
.toList();
}
/// Read file normalizing CRLF to LF.
String readFile(File file) => file.readAsStringSync().replaceAll('\r\n', '\n');
/// Compares 2 hierarchies using `git diff --no-index`.
void comparePaths(String path1, String path2) {
final diffCommand = [
'diff',
'--no-index',
if (stderr.supportsAnsiEscapes) '--color=always',
];
final diffProc = Process.runSync('git', [...diffCommand, path1, path2]);
if (diffProc.exitCode != 0) {
final originalDiff = diffProc.stdout;
log.warning(
'Paths $path1 and $path2 differ, Running dart format on $path1.');
Process.runSync('dart', ['format', path1]);
final fallbackDiffProc =
Process.runSync('git', [...diffCommand, path1, path2]);
if (fallbackDiffProc.exitCode != 0) {
stderr.writeln(originalDiff);
throw Exception('Paths $path1 and $path2 differ');
}
}
}
Future<void> _generateTempBindings(Config config, Directory tempDir) async {
final singleFile =
config.outputConfig.dartConfig.structure == OutputStructure.singleFile;
final tempLib = singleFile
? tempDir.uri.resolve('generated.dart')
: tempDir.uri.resolve('lib/');
config.outputConfig.dartConfig.path = tempLib;
config.logLevel = Level.WARNING;
await generateJniBindings(config);
}
/// Generates and compares bindings with reference bindings.
///
/// `dartReferenceBindings` can be directory or file depending on output
/// configuration.
Future<void> generateAndCompareBindings(Config config) async {
final dartReferenceBindings =
config.outputConfig.dartConfig.path.toFilePath();
final currentDir = Directory.current;
final tempDir = currentDir.createTempSync('jnigen_test_temp');
final singleFile =
config.outputConfig.dartConfig.structure == OutputStructure.singleFile;
final tempLib = singleFile
? tempDir.uri.resolve('generated.dart')
: tempDir.uri.resolve('lib/');
try {
await _generateTempBindings(config, tempDir);
comparePaths(dartReferenceBindings, tempLib.toFilePath());
} finally {
tempDir.deleteSync(recursive: true);
}
}
Future<void> generateAndAnalyzeBindings(Config config,
{Iterable<String> confirmExists = const []}) async {
final tempDir = Directory.current.createTempSync('jnigen_test_temp');
try {
await _generateTempBindings(config, tempDir);
final analyzeResult = Process.runSync('dart', ['analyze', tempDir.path]);
if (analyzeResult.exitCode != 0) {
stderr.write(analyzeResult.stdout);
fail('Analyzer exited with non-zero status (${analyzeResult.exitCode})');
}
final singleFile =
config.outputConfig.dartConfig.structure == OutputStructure.singleFile;
if (!singleFile && confirmExists.isNotEmpty) {
throw UnimplementedError('Currently only supports single file mode '
'for confirming that classes exists');
} else if (singleFile && confirmExists.isNotEmpty) {
final generatedCode =
await File.fromUri(tempDir.uri.resolve('generated.dart'))
.readAsString();
for (final className in confirmExists) {
if (!generatedCode.contains(className)) {
fail('$className does not exist in the generated bindings');
}
}
}
} finally {
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.');
}
}
const bindingTests = [
'jackson_core_test',
'simple_package_test',
'kotlin_test',
];
const registrantName = 'runtime_test_registrant.dart';
/// Verifies if locally built dependencies (currently `ApiSummarizer`)
/// are up-to-date.
Future<void> checkLocallyBuiltDependencies() async {
await failIfSummarizerNotBuilt();
}
void generateAndCompare(
String description,
Config config,
) {
test(description, () async {
await generateAndCompareBindings(config);
}, timeout: const Timeout(Duration(minutes: 2)));
}
List<String> findFilesWithSuffix(Directory dir, String suffix) {
return dir
.listSync(recursive: true)
.map((entry) => relative(entry.path, from: dir.path))
.where((path) => path.endsWith(suffix))
.toList();
}
Future<void> compileJavaFiles(Directory root, Directory target) async {
final javaFiles = findFilesWithSuffix(root, '.java');
await runCommand(
'javac',
['-d', target.absolute.path, ...javaFiles],
workingDirectory: root.path,
messageOnFailure: 'Cannot compile java sources',
);
}