flutter/packages/flutter_tools/test/commands/create_test.dart
Ian Hickson 3dec6a6930
Clean up usage of temporary directories (#20682)
All temporary directory start with `flutter_` and have their random component separated from the name by a period, as in `flutter_test_bundle.YFYQMY`.

I've tried to find some of the places where we didn't cleanly delete temporary directories, too. This greatly reduces, though it does not entirely eliminate, the directories we leave behind when running tests, especially `flutter_tools` tests.

While I was at it I standardized on `tempDir` as the variable name for temporary directories, since it was the most common, removing occurrences of `temp` and `tmp`, among others.

Also I factored out some common code that used to catch exceptions that happen on Windows, and made more places use that pattern.
2018-08-17 13:17:23 -07:00

575 lines
21 KiB
Dart

// Copyright 2015 The Chromium 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:async';
import 'dart:convert';
import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/dart/sdk.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../src/common.dart';
import '../src/context.dart';
const String frameworkRevision = '12345678';
const String frameworkChannel = 'omega';
void main() {
group('create', () {
Directory tempDir;
Directory projectDir;
FlutterVersion mockFlutterVersion;
LoggingProcessManager loggingProcessManager;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
loggingProcessManager = new LoggingProcessManager();
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
projectDir = tempDir.childDirectory('flutter_project');
mockFlutterVersion = new MockFlutterVersion();
});
tearDown(() {
tryToDelete(tempDir);
});
// Verify that we create a project that is well-formed.
testUsingContext('project', () async {
await _createAndAnalyzeProject(
projectDir,
<String>[],
<String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
'ios/Runner/AppDelegate.h',
'ios/Runner/AppDelegate.m',
'ios/Runner/main.m',
'lib/main.dart',
'test/widget_test.dart',
'flutter_project.iml',
],
);
return _runFlutterTest(projectDir);
}, timeout: allowForRemotePubInvocation);
testUsingContext('kotlin/swift project', () async {
return _createProject(
projectDir,
<String>['--no-pub', '--android-language', 'kotlin', '-i', 'swift'],
<String>[
'android/app/src/main/kotlin/com/example/flutterproject/MainActivity.kt',
'ios/Runner/AppDelegate.swift',
'ios/Runner/Runner-Bridging-Header.h',
'lib/main.dart',
],
unexpectedPaths: <String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
'ios/Runner/AppDelegate.h',
'ios/Runner/AppDelegate.m',
'ios/Runner/main.m',
],
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('package project', () async {
await _createAndAnalyzeProject(
projectDir,
<String>['--template=package'],
<String>[
'lib/flutter_project.dart',
'test/flutter_project_test.dart',
],
unexpectedPaths: <String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'ios/Runner/AppDelegate.h',
'ios/Runner/AppDelegate.m',
'ios/Runner/main.m',
'lib/main.dart',
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
'example/lib/main.dart',
'test/widget_test.dart',
],
);
return _runFlutterTest(projectDir);
}, timeout: allowForRemotePubInvocation);
testUsingContext('plugin project', () async {
await _createAndAnalyzeProject(
projectDir,
<String>['--template=plugin'],
<String>[
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'lib/flutter_project.dart',
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
'example/lib/main.dart',
'flutter_project.iml',
],
plugin: true,
);
return _runFlutterTest(projectDir.childDirectory('example'));
}, timeout: allowForRemotePubInvocation);
testUsingContext('kotlin/swift plugin project', () async {
return _createProject(
projectDir,
<String>['--no-pub', '--template=plugin', '-a', 'kotlin', '--ios-language', 'swift'],
<String>[
'android/src/main/kotlin/com/example/flutterproject/FlutterProjectPlugin.kt',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'ios/Classes/SwiftFlutterProjectPlugin.swift',
'lib/flutter_project.dart',
'example/android/app/src/main/kotlin/com/example/flutterprojectexample/MainActivity.kt',
'example/ios/Runner/AppDelegate.swift',
'example/ios/Runner/Runner-Bridging-Header.h',
'example/lib/main.dart',
],
unexpectedPaths: <String>[
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
],
plugin: true,
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('plugin project with custom org', () async {
return _createProject(
projectDir,
<String>['--no-pub', '--template=plugin', '--org', 'com.bar.foo'],
<String>[
'android/src/main/java/com/bar/foo/flutterproject/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/bar/foo/flutterprojectexample/MainActivity.java',
],
unexpectedPaths: <String>[
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
],
plugin: true,
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('project with-driver-test', () async {
return _createAndAnalyzeProject(
projectDir,
<String>['--with-driver-test'],
<String>['lib/main.dart'],
);
}, timeout: allowForRemotePubInvocation);
testUsingContext('module', () async {
return _createProject(
projectDir,
<String>['--no-pub', '--template=module'],
<String>[
'.gitignore',
'.metadata',
'lib/main.dart',
'pubspec.yaml',
'README.md',
],
unexpectedPaths: <String>[
'.android/',
'android/',
'ios/',
]
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('module with pub', () async {
return _createProject(
projectDir,
<String>['-t', 'module'],
<String>[
'.gitignore',
'.metadata',
'lib/main.dart',
'pubspec.lock',
'pubspec.yaml',
'README.md',
'.packages',
'.android/build.gradle',
'.android/Flutter/build.gradle',
'.android/Flutter/src/main/java/io/flutter/facade/Flutter.java',
'.android/Flutter/src/main/java/io/flutter/facade/FlutterFragment.java',
'.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
'.android/Flutter/src/main/AndroidManifest.xml',
'.android/gradle.properties',
'.android/gradle/wrapper/gradle-wrapper.jar',
'.android/gradle/wrapper/gradle-wrapper.properties',
'.android/gradlew',
'.android/gradlew.bat',
'.android/local.properties',
'.android/include_flutter.groovy',
'.android/settings.gradle',
],
unexpectedPaths: <String>[
'android/',
'ios/',
]
);
}, timeout: allowForRemotePubInvocation);
// Verify content and formatting
testUsingContext('content', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--org', 'com.foo.bar', projectDir.path]);
void expectExists(String relPath) {
expect(fs.isFileSync('${projectDir.path}/$relPath'), true);
}
expectExists('lib/main.dart');
for (FileSystemEntity file in projectDir.listSync(recursive: true)) {
if (file is File && file.path.endsWith('.dart')) {
final String original = file.readAsStringSync();
final Process process = await Process.start(
sdkBinaryName('dartfmt'),
<String>[file.path],
workingDirectory: projectDir.path,
);
final String formatted = await process.stdout.transform(utf8.decoder).join();
expect(original, formatted, reason: file.path);
}
}
await _runFlutterTest(projectDir, target: fs.path.join(projectDir.path, 'test', 'widget_test.dart'));
// Generated Xcode settings
final String xcodeConfigPath = fs.path.join('ios', 'Flutter', 'Generated.xcconfig');
expectExists(xcodeConfigPath);
final File xcodeConfigFile = fs.file(fs.path.join(projectDir.path, xcodeConfigPath));
final String xcodeConfig = xcodeConfigFile.readAsStringSync();
expect(xcodeConfig, contains('FLUTTER_ROOT='));
expect(xcodeConfig, contains('FLUTTER_APPLICATION_PATH='));
expect(xcodeConfig, contains('FLUTTER_FRAMEWORK_DIR='));
// App identification
final String xcodeProjectPath = fs.path.join('ios', 'Runner.xcodeproj', 'project.pbxproj');
expectExists(xcodeProjectPath);
final File xcodeProjectFile = fs.file(fs.path.join(projectDir.path, xcodeProjectPath));
final String xcodeProject = xcodeProjectFile.readAsStringSync();
expect(xcodeProject, contains('PRODUCT_BUNDLE_IDENTIFIER = com.foo.bar.flutterProject'));
final String versionPath = fs.path.join('.metadata');
expectExists(versionPath);
final String version = fs.file(fs.path.join(projectDir.path, versionPath)).readAsStringSync();
expect(version, contains('version:'));
expect(version, contains('revision: 12345678'));
expect(version, contains('channel: omega'));
// IntelliJ metadata
final String intelliJSdkMetadataPath = fs.path.join('.idea', 'libraries', 'Dart_SDK.xml');
expectExists(intelliJSdkMetadataPath);
final String sdkMetaContents = fs.file(fs.path.join(projectDir.path, intelliJSdkMetadataPath)).readAsStringSync();
expect(sdkMetaContents, contains('<root url="file:/'));
expect(sdkMetaContents, contains('/bin/cache/dart-sdk/lib/core"'));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockFlutterVersion,
}, timeout: allowForCreateFlutterProject);
// Verify that we can regenerate over an existing project.
testUsingContext('can re-gen over existing project', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', projectDir.path]);
await runner.run(<String>['create', '--no-pub', projectDir.path]);
}, timeout: allowForCreateFlutterProject);
testUsingContext('can re-gen android/ folder, reusing custom org', () async {
await _createProject(
projectDir,
<String>['--no-pub', '--org', 'com.bar.foo'],
<String>[],
);
projectDir.childDirectory('android').deleteSync(recursive: true);
return _createProject(
projectDir,
<String>['--no-pub'],
<String>[
'android/app/src/main/java/com/bar/foo/flutterproject/MainActivity.java',
],
unexpectedPaths: <String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
],
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('can re-gen ios/ folder, reusing custom org', () async {
await _createProject(
projectDir,
<String>['--no-pub', '--org', 'com.bar.foo'],
<String>[],
);
projectDir.childDirectory('ios').deleteSync(recursive: true);
await _createProject(projectDir, <String>['--no-pub'], <String>[]);
final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
expect(
await project.ios.productBundleIdentifier(),
'com.bar.foo.flutterProject',
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org', () async {
await _createProject(
projectDir,
<String>['--no-pub', '-t', 'plugin', '--org', 'com.bar.foo'],
<String>[],
);
projectDir.childDirectory('example').deleteSync(recursive: true);
projectDir.childDirectory('ios').deleteSync(recursive: true);
await _createProject(
projectDir,
<String>['--no-pub', '-t', 'plugin'],
<String>[
'example/android/app/src/main/java/com/bar/foo/flutterprojectexample/MainActivity.java',
'ios/Classes/FlutterProjectPlugin.h',
],
unexpectedPaths: <String>[
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
],
);
final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
expect(
await project.example.ios.productBundleIdentifier(),
'com.bar.foo.flutterProjectExample',
);
}, timeout: allowForCreateFlutterProject);
testUsingContext('fails to re-gen without specified org when org is ambiguous', () async {
await _createProject(
projectDir,
<String>['--no-pub', '--org', 'com.bar.foo'],
<String>[],
);
fs.directory(fs.path.join(projectDir.path, 'ios')).deleteSync(recursive: true);
await _createProject(
projectDir,
<String>['--no-pub', '--org', 'com.bar.baz'],
<String>[],
);
expect(
() => _createProject(projectDir, <String>[], <String>[]),
throwsToolExit(message: 'Ambiguous organization'),
);
}, timeout: allowForCreateFlutterProject);
// Verify that we help the user correct an option ordering issue
testUsingContext('produces sensible error message', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
expect(
runner.run(<String>['create', projectDir.path, '--pub']),
throwsToolExit(exitCode: 2, message: 'Try moving --pub'),
);
});
// Verify that we fail with an error code when the file exists.
testUsingContext('fails when file exists', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
final File existingFile = fs.file('${projectDir.path.toString()}/bad');
if (!existingFile.existsSync())
existingFile.createSync(recursive: true);
expect(
runner.run(<String>['create', existingFile.path]),
throwsToolExit(message: 'file exists'),
);
});
testUsingContext('fails when invalid package name', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
expect(
runner.run(<String>['create', fs.path.join(projectDir.path, 'invalidName')]),
throwsToolExit(message: '"invalidName" is not a valid Dart package name.'),
);
});
testUsingContext('invokes pub offline when requested', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--pub', '--offline', projectDir.path]);
expect(loggingProcessManager.commands.first, contains(matches(r'dart-sdk[\\/]bin[\\/]pub')));
expect(loggingProcessManager.commands.first, contains('--offline'));
},
timeout: allowForCreateFlutterProject,
overrides: <Type, Generator>{
ProcessManager: () => loggingProcessManager,
},
);
testUsingContext('invokes pub online when offline not requested', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--pub', projectDir.path]);
expect(loggingProcessManager.commands.first, contains(matches(r'dart-sdk[\\/]bin[\\/]pub')));
expect(loggingProcessManager.commands.first, isNot(contains('--offline')));
},
timeout: allowForCreateFlutterProject,
overrides: <Type, Generator>{
ProcessManager: () => loggingProcessManager,
},
);
});
}
Future<Null> _createProject(
Directory dir, List<String> createArgs, List<String> expectedPaths,
{ List<String> unexpectedPaths = const <String>[], bool plugin = false}) async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
final List<String> args = <String>['create'];
args.addAll(createArgs);
args.add(dir.path);
await runner.run(args);
bool pathExists(String path) {
final String fullPath = fs.path.join(dir.path, path);
return fs.typeSync(fullPath) != FileSystemEntityType.notFound;
}
for (String path in expectedPaths) {
expect(pathExists(path), true, reason: '$path does not exist');
}
for (String path in unexpectedPaths) {
expect(pathExists(path), false, reason: '$path exists');
}
}
Future<Null> _createAndAnalyzeProject(
Directory dir, List<String> createArgs, List<String> expectedPaths,
{ List<String> unexpectedPaths = const <String>[], bool plugin = false }) async {
await _createProject(dir, createArgs, expectedPaths, unexpectedPaths: unexpectedPaths, plugin: plugin);
if (plugin) {
await _analyzeProject(dir.path);
} else {
await _analyzeProject(dir.path);
}
}
Future<Null> _analyzeProject(String workingDir) async {
final String flutterToolsPath = fs.path.absolute(fs.path.join(
'bin',
'flutter_tools.dart',
));
final List<String> args = <String>[]
..addAll(dartVmFlags)
..add(flutterToolsPath)
..add('analyze');
final ProcessResult exec = await Process.run(
'$dartSdkPath/bin/dart',
args,
workingDirectory: workingDir,
);
if (exec.exitCode != 0) {
print(exec.stdout);
print(exec.stderr);
}
expect(exec.exitCode, 0);
}
Future<Null> _runFlutterTest(Directory workingDir, {String target}) async {
final String flutterToolsPath = fs.path.absolute(fs.path.join(
'bin',
'flutter_tools.dart',
));
final List<String> args = <String>[]
..addAll(dartVmFlags)
..add(flutterToolsPath)
..add('test')
..add('--no-color');
if (target != null)
args.add(target);
final ProcessResult exec = await Process.run(
'$dartSdkPath/bin/dart',
args,
workingDirectory: workingDir.path,
);
if (exec.exitCode != 0) {
print(exec.stdout);
print(exec.stderr);
}
expect(exec.exitCode, 0);
}
class MockFlutterVersion extends Mock implements FlutterVersion {}
/// A ProcessManager that invokes a real process manager, but keeps
/// track of all commands sent to it.
class LoggingProcessManager extends LocalProcessManager {
List<List<String>> commands = <List<String>>[];
@override
Future<Process> start(
List<dynamic> command, {
String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) {
commands.add(command);
return super.start(
command,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
mode: mode,
);
}
}