| // Copyright 2014 The Flutter Authors. 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| import 'package:process_runner/process_runner.dart'; |
| |
| // This program enables testing of private interfaces in the flutter package. |
| // |
| // See README.md for more information. |
| |
| final Directory flutterRoot = |
| Directory(path.fromUri(Platform.script)).absolute.parent.parent.parent.parent.parent; |
| final Directory flutterPackageDir = Directory(path.join(flutterRoot.path, 'packages', 'flutter')); |
| final Directory testPrivateDir = Directory(path.join(flutterPackageDir.path, 'test_private')); |
| final Directory privateTestsDir = Directory(path.join(testPrivateDir.path, 'test')); |
| |
| void _usage() { |
| print('Usage: test_private.dart [--help] [--temp-dir=<temp_dir>]'); |
| print(''' |
| --help Print a usage message. |
| --temp-dir A location where temporary files may be written. Defaults to a |
| directory in the system temp folder. If a temp_dir is not |
| specified, then the default temp_dir will be created, used, and |
| removed automatically. |
| '''); |
| } |
| |
| Future<void> main(List<String> args) async { |
| // TODO(gspencergoog): Convert to using the args package once it has been |
| // converted to be non-nullable by default. |
| if (args.isNotEmpty && args[0] == '--help') { |
| _usage(); |
| exit(0); |
| } |
| |
| void errorExit(String message, {int exitCode = -1}) { |
| stderr.write('Error: $message\n\n'); |
| _usage(); |
| exit(exitCode); |
| } |
| |
| if (args.length > 2) { |
| errorExit('Too many arguments.'); |
| } |
| |
| String? tempDirArg; |
| if (args.isNotEmpty) { |
| if (args[0].startsWith('--temp-dir')) { |
| if (args[0].startsWith('--temp-dir=')) { |
| tempDirArg = args[0].replaceFirst('--temp-dir=', ''); |
| } else { |
| if (args.length < 2) { |
| errorExit('Not enough arguments to --temp-dir'); |
| } |
| tempDirArg = args[1]; |
| } |
| } else { |
| errorExit('Invalid arguments ${args.join(' ')}.'); |
| } |
| } |
| |
| Directory tempDir; |
| bool removeTempDir = false; |
| if (tempDirArg == null || tempDirArg.isEmpty) { |
| tempDir = Directory.systemTemp.createTempSync('flutter_package.'); |
| removeTempDir = true; |
| } else { |
| tempDir = Directory(tempDirArg); |
| if (!tempDir.existsSync()) { |
| errorExit("Temporary directory $tempDirArg doesn't exist."); |
| } |
| } |
| |
| bool success = true; |
| try { |
| await for (final TestCase testCase in getTestCases(tempDir)) { |
| stderr.writeln('Analyzing test case $testCase'); |
| if (!testCase.setUp()) { |
| stderr.writeln('Unable to set up $testCase'); |
| success = false; |
| break; |
| } |
| if (!await testCase.runAnalyzer()) { |
| stderr.writeln('Test case $testCase failed analysis.'); |
| success = false; |
| break; |
| } else { |
| stderr.writeln('Test case $testCase passed analysis.'); |
| } |
| stderr.writeln('Running test case $testCase'); |
| if (!await testCase.runTests()) { |
| stderr.writeln('Test case $testCase failed.'); |
| success = false; |
| break; |
| } else { |
| stderr.writeln('Test case $testCase succeeded.'); |
| } |
| } |
| } finally { |
| if (removeTempDir) { |
| tempDir.deleteSync(recursive: true); |
| } |
| } |
| exit(success ? 0 : 1); |
| } |
| |
| File makeAbsolute(File file, {Directory? workingDirectory}) { |
| workingDirectory ??= Directory.current; |
| return File(path.join(workingDirectory.absolute.path, file.path)); |
| } |
| |
| /// A test case representing a private test file that should be run. |
| /// |
| /// It is loaded from a JSON manifest file that contains a list of dependencies |
| /// to copy, a list of test files themselves, and a pubspec file. |
| /// |
| /// The dependencies are copied into the test area with the same relative path. |
| /// |
| /// The test files are copied to the root of the test area. |
| /// |
| /// The pubspec file is copied to the root of the test area too, but renamed to |
| /// "pubspec.yaml". |
| class TestCase { |
| TestCase.fromManifest(this.manifest, this.tmpdir) { |
| _json = jsonDecode(manifest.readAsStringSync()) as Map<String, dynamic>; |
| tmpdir.createSync(recursive: true); |
| assert(tmpdir.existsSync()); |
| } |
| |
| final File manifest; |
| final Directory tmpdir; |
| |
| Map<String, dynamic> _json = <String, dynamic>{}; |
| |
| Iterable<File> _getList(String name) sync* { |
| for (final dynamic entry in _json[name] as List<dynamic>) { |
| final String name = entry as String; |
| yield File(path.joinAll(name.split('/'))); |
| } |
| } |
| |
| Iterable<File> get dependencies => _getList('deps'); |
| Iterable<File> get testDependencies => _getList('test_deps'); |
| Iterable<File> get tests => _getList('tests'); |
| File get pubspec => File(_json['pubspec'] as String); |
| |
| bool setUp() { |
| // Copy the manifest tests and deps to the same relative path under the |
| // tmpdir. |
| for (final File file in dependencies) { |
| try { |
| final Directory destDir = Directory(path.join(tmpdir.absolute.path, file.parent.path)); |
| destDir.createSync(recursive: true); |
| final File absFile = makeAbsolute(file, workingDirectory: flutterPackageDir); |
| final String destination = path.join(tmpdir.absolute.path, file.path); |
| absFile.copySync(destination); |
| } on FileSystemException catch (e) { |
| stderr.writeln('Problem copying manifest dep file ${file.path} to ${tmpdir.path}: $e'); |
| return false; |
| } |
| } |
| for (final File file in testDependencies) { |
| try { |
| final Directory destDir = Directory(path.join(tmpdir.absolute.path, 'lib', file.parent.path)); |
| destDir.createSync(recursive: true); |
| final File absFile = makeAbsolute(file, workingDirectory: flutterPackageDir); |
| final String destination = path.join(tmpdir.absolute.path, 'lib', file.path); |
| absFile.copySync(destination); |
| } on FileSystemException catch (e) { |
| stderr.writeln('Problem copying manifest test_dep file ${file.path} to ${tmpdir.path}: $e'); |
| return false; |
| } |
| } |
| // Copy the test files into the tmpdir's lib directory. |
| for (final File file in tests) { |
| String destination = tmpdir.path; |
| try { |
| final File absFile = makeAbsolute(file, workingDirectory: privateTestsDir); |
| // Copy the file, but without the ".tmpl" extension. |
| destination = path.join(tmpdir.absolute.path, 'lib', path.basenameWithoutExtension(file.path)); |
| absFile.copySync(destination); |
| } on FileSystemException catch (e) { |
| stderr.writeln('Problem copying test ${file.path} to $destination: $e'); |
| return false; |
| } |
| } |
| |
| // Copy the pubspec to the right place. |
| makeAbsolute(pubspec, workingDirectory: privateTestsDir) |
| .copySync(path.join(tmpdir.absolute.path, 'pubspec.yaml')); |
| |
| // Use Flutter's analysis_options.yaml file from packages/flutter. |
| File(path.join(tmpdir.absolute.path, 'analysis_options.yaml')) |
| .writeAsStringSync('include: ${path.toUri(path.join(flutterRoot.path, 'packages', 'flutter', 'analysis_options.yaml'))}'); |
| |
| return true; |
| } |
| |
| Future<bool> runAnalyzer() async { |
| final String flutter = path.join(flutterRoot.path, 'bin', 'flutter'); |
| final ProcessRunner runner = ProcessRunner( |
| defaultWorkingDirectory: tmpdir.absolute, |
| printOutputDefault: true, |
| ); |
| final ProcessRunnerResult result = await runner.runProcess( |
| <String>[flutter, 'analyze', '--current-package', '--pub', '--congratulate', '.'], |
| failOk: true, |
| ); |
| if (result.exitCode != 0) { |
| return false; |
| } |
| return true; |
| } |
| |
| Future<bool> runTests() async { |
| final ProcessRunner runner = ProcessRunner( |
| defaultWorkingDirectory: tmpdir.absolute, |
| printOutputDefault: true, |
| ); |
| final String flutter = path.join(flutterRoot.path, 'bin', 'flutter'); |
| for (final File test in tests) { |
| final String testPath = path.join(path.dirname(test.path), 'lib', path.basenameWithoutExtension(test.path)); |
| final ProcessRunnerResult result = await runner.runProcess( |
| <String>[flutter, 'test', '--enable-experiment=non-nullable', '--no-sound-null-safety', '--null-assertions', testPath], |
| failOk: true, |
| ); |
| if (result.exitCode != 0) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @override |
| String toString() { |
| return path.basenameWithoutExtension(manifest.path); |
| } |
| } |
| |
| Stream<TestCase> getTestCases(Directory tmpdir) async* { |
| final Directory testDir = Directory(path.join(testPrivateDir.path, 'test')); |
| await for (final FileSystemEntity entity in testDir.list(recursive: true)) { |
| if (path.split(entity.path).where((String element) => element.startsWith('.')).isNotEmpty) { |
| // Skip hidden files, directories, and the files inside them, like |
| // .dart_tool, which contains a (non-hidden) .json file. |
| continue; |
| } |
| if (entity is File && path.basename(entity.path).endsWith('_test.json')) { |
| print('Found manifest ${entity.path}'); |
| final Directory testTmpDir = |
| Directory(path.join(tmpdir.absolute.path, path.basenameWithoutExtension(entity.path))); |
| yield TestCase.fromManifest(entity, testTmpDir); |
| } |
| } |
| } |