blob: 7005e34926db329280501d12287819f065edb35b [file] [log] [blame] [edit]
// 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);
}
}
}