| // Copyright (c) 2019, 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. |
| |
| // @dart = 2.9 |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| |
| // TODO(dacoharkes): Migrate script to be nullsafe and generate nullsafe |
| // Flutter app. |
| |
| main(List<String> args) async { |
| if (args.length != 1) { |
| print('Usage ${Platform.executable} ${Platform.script} <output-dir>'); |
| exit(1); |
| } |
| |
| final sdkRoot = |
| path.canonicalize(path.join(Platform.script.path, '../../..')); |
| final flutterTestsDir = args.single; |
| |
| print('Using SDK root: $sdkRoot'); |
| final testFiles = <String>[]; |
| final failedOrTimedOut = <String>[]; |
| final filteredTests = <String>[]; |
| await for (final testFile in listTestFiles(sdkRoot, filteredTests)) { |
| final duration = await run(sdkRoot, testFile); |
| if (duration != null && duration.inSeconds < 5) { |
| testFiles.add(testFile); |
| } else { |
| failedOrTimedOut.add(testFile); |
| } |
| } |
| testFiles.sort(); |
| failedOrTimedOut.sort(); |
| filteredTests.sort(); |
| |
| dumpTestList(testFiles, 'The following tests will be included:'); |
| dumpTestList(failedOrTimedOut, |
| 'The following tests will be excluded due to timeout or test failure:'); |
| dumpTestList( |
| filteredTests, |
| 'The following tests were filtered due to using ' |
| 'dart_api.h/async/DynamicLibrary.{process,executable}/...'); |
| |
| final allFiles = <String>{}; |
| allFiles.add(path.join(sdkRoot, 'pkg/expect/lib/expect.dart')); |
| for (final testFile in testFiles) { |
| allFiles.add(testFile); |
| await addImportedFilesTo(allFiles, testFile); |
| } |
| |
| await generateCleanDir(flutterTestsDir); |
| |
| final dartTestsDirRelative = 'example/lib'; |
| final dartTestsDir = path.join(flutterTestsDir, dartTestsDirRelative); |
| await generateDartTests(dartTestsDir, allFiles, testFiles); |
| |
| final ccDirRelative = 'ios/Classes'; |
| final ccDir = path.join(flutterTestsDir, ccDirRelative); |
| await generateCLibs(sdkRoot, ccDir, allFiles, testFiles); |
| |
| print(''' |
| |
| Files generated in: |
| * $dartTestsDir |
| * $ccDir |
| |
| Generate flutter test application with: |
| flutter create --platforms=android,ios --template=plugin <ffi_test_app_name> |
| |
| Please copy generated files into FFI flutter test application: |
| cd <ffi_test_app_dir> && cp -r $flutterTestsDir ./ |
| |
| After copying modify the test application: |
| * Modify example/pubspec.yaml to depend on package:ffi. |
| * Modify example/lib/main.dart to invoke all.dart while rendering. |
| * Open example/ios/Runner.xcworkspace in Xcode. |
| * Add the cpp files to Pods/Development Pods/<deep nesting>/ios/Classes |
| to ensure they are statically linked to the app. |
| '''); |
| // TODO(dacoharkes): Automate these steps. How to automate the XCode step? |
| } |
| |
| void dumpTestList(List<String> testFiles, String message) { |
| if (testFiles.isEmpty) return; |
| |
| print(message); |
| for (final testFile in testFiles) { |
| print(' ${path.basename(testFile)}'); |
| } |
| } |
| |
| final importRegExp = RegExp(r'''^import.*['"](.+)['"].*;'''); |
| |
| Future addImportedFilesTo(Set<String> allFiles, String testFile) async { |
| final content = await File(testFile).readAsString(); |
| for (final line in content.split('\n')) { |
| final match = importRegExp.matchAsPrefix(line); |
| if (match != null) { |
| final filename = match.group(1); |
| if (!filename.contains('dart:') && |
| !filename.contains('package:expect') && |
| !filename.contains('package:ffi')) { |
| final importedFile = Uri.file(testFile).resolve(filename).toFilePath(); |
| if (allFiles.add(importedFile)) { |
| addImportedFilesTo(allFiles, importedFile); |
| } |
| } |
| } |
| } |
| } |
| |
| Future generateCLibs(String sdkRoot, String destDir, Set<String> allFiles, |
| List<String> testFiles) async { |
| final dir = await generateCleanDir(destDir); |
| |
| String destinationFile; |
| |
| final lib1 = |
| path.join(sdkRoot, 'runtime/bin/ffi_test/ffi_test_dynamic_library.cc'); |
| destinationFile = |
| path.join(dir.path, path.basename(lib1)).replaceAll('.cc', '.cpp'); |
| File(destinationFile).writeAsStringSync(File(lib1).readAsStringSync()); |
| |
| final lib2 = path.join(sdkRoot, 'runtime/bin/ffi_test/ffi_test_functions.cc'); |
| destinationFile = |
| path.join(dir.path, path.basename(lib2)).replaceAll('.cc', '.cpp'); |
| File(destinationFile).writeAsStringSync(File(lib2).readAsStringSync()); |
| |
| final lib3 = path.join( |
| sdkRoot, 'runtime/bin/ffi_test/ffi_test_functions_generated.cc'); |
| destinationFile = |
| path.join(dir.path, path.basename(lib3)).replaceAll('.cc', '.cpp'); |
| File(destinationFile).writeAsStringSync(File(lib3).readAsStringSync()); |
| } |
| |
| String cleanDart(String content) { |
| return content.replaceAll('package:expect/expect.dart', 'expect.dart'); |
| } |
| |
| Future generateDartTests( |
| String destDir, Set<String> allFiles, List<String> testFiles) async { |
| final dir = await generateCleanDir(destDir); |
| |
| final sink = File(path.join(dir.path, 'all.dart')).openWrite(); |
| sink.writeln('import "dart:async";'); |
| sink.writeln(''); |
| for (int i = 0; i < testFiles.length; ++i) { |
| sink.writeln('import "${path.basename(testFiles[i])}" as main$i;'); |
| } |
| sink.writeln(''); |
| sink.writeln('Future invoke(dynamic fun) async {'); |
| sink.writeln(' if (fun is void Function() || fun is Future Function()) {'); |
| sink.writeln(' return await fun();'); |
| sink.writeln(' } else {'); |
| sink.writeln(' return await fun(<String>[]);'); |
| sink.writeln(' }'); |
| sink.writeln('}'); |
| sink.writeln(''); |
| sink.writeln('dynamic main() async {'); |
| for (int i = 0; i < testFiles.length; ++i) { |
| sink.writeln(' await invoke(main$i.main);'); |
| } |
| sink.writeln('}'); |
| await sink.close(); |
| |
| for (final file in allFiles) { |
| File(path.join(dir.path, path.basename(file))) |
| .writeAsStringSync(cleanDart(File(file).readAsStringSync())); |
| } |
| |
| File(path.join(dir.path, 'dylib_utils.dart')).writeAsStringSync(''' |
| import 'dart:ffi' as ffi; |
| import 'dart:io' show Platform; |
| |
| ffi.DynamicLibrary dlopenPlatformSpecific(String name, {String path}) { |
| return Platform.isAndroid |
| ? ffi.DynamicLibrary.open('libffi_tests.so') |
| : ffi.DynamicLibrary.process(); |
| } |
| '''); |
| } |
| |
| Stream<String> listTestFiles( |
| String sdkRoot, List<String> filteredTests) async* { |
| await for (final file |
| in Directory(path.join(sdkRoot, 'tests/ffi_2')).list()) { |
| if (file is File && file.path.endsWith('_test.dart')) { |
| // These tests are VM specific and cannot necessarily be run on Flutter. |
| if (path.basename(file.path).startsWith('vmspecific_')) { |
| filteredTests.add(file.path); |
| continue; |
| } |
| // These tests use special features which are hard to test on Flutter. |
| final contents = file.readAsStringSync(); |
| if (contents.contains(RegExp('//# .* compile-time error')) || |
| contents.contains('DynamicLibrary.process') || |
| contents.contains('DynamicLibrary.executable')) { |
| filteredTests.add(file.path); |
| continue; |
| } |
| yield file.path; |
| } |
| } |
| } |
| |
| Future<Duration> run(String sdkRoot, String testFile) async { |
| final env = Map<String, String>.from(Platform.environment); |
| env['LD_LIBRARY_PATH'] = path.join(sdkRoot, 'out/ReleaseX64'); |
| final sw = Stopwatch()..start(); |
| final Process process = await Process.start( |
| Platform.executable, <String>[testFile], |
| environment: env); |
| final timer = Timer(const Duration(seconds: 3), () => process.kill()); |
| process.stdout.listen((_) {}); |
| process.stderr.listen((_) {}); |
| if (await process.exitCode != 0) return null; |
| timer.cancel(); |
| return sw.elapsed; |
| } |
| |
| Future<Directory> generateCleanDir(String dirname) async { |
| final directory = Directory(dirname); |
| if (await directory.exists()) { |
| await directory.delete(recursive: true); |
| } |
| await directory.create(recursive: true); |
| return directory; |
| } |