blob: cd3a4b44e224c9fe117b801f2dcf6d5f9e983a5a [file] [log] [blame]
// Copyright (c) 2020, 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:async';
import 'dart:io';
import 'package:analysis_server/src/edit/fix/non_nullable_fix.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart' as mock_sdk;
import 'package:cli_util/cli_logging.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:nnbd_migration/migration_cli.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(_MigrationCliTestPosix);
defineReflectiveTests(_MigrationCliTestWindows);
});
}
class _MigrationCli extends MigrationCli {
Future<void> Function() _runWhilePreviewServerActive;
_MigrationCli(_MigrationCliTestBase test)
: super(
binaryName: 'nnbd_migration',
loggerFactory: (isVerbose) => test.logger = _TestLogger(isVerbose),
defaultSdkPathOverride:
test.resourceProvider.convertPath(mock_sdk.sdkRoot),
resourceProvider: test.resourceProvider);
@override
Future<void> blockUntilSignalInterrupt() async {
if (_runWhilePreviewServerActive == null) {
fail('Preview server not expected to have been started');
}
await _runWhilePreviewServerActive.call();
_runWhilePreviewServerActive = null;
}
Future<void> runWithPreviewServer(
List<String> args, Future<void> callback()) async {
_runWhilePreviewServerActive = callback;
await run(args);
if (_runWhilePreviewServerActive != null) {
fail('Preview server never started');
}
}
}
abstract class _MigrationCliTestBase {
void set logger(_TestLogger logger);
MemoryResourceProvider get resourceProvider;
}
mixin _MigrationCliTestMethods on _MigrationCliTestBase {
@override
/*late*/ _TestLogger logger;
final hasVerboseHelpMessage = contains('for verbose help output');
final hasUsageText = contains('Usage: nnbd_migration');
String assertErrorExit(MigrationCli cli, {bool withUsage = true}) {
expect(cli.exitCode, isNotNull);
expect(cli.exitCode, isNot(0));
var stderrText = logger.stderrBuffer.toString();
expect(stderrText, withUsage ? hasUsageText : isNot(hasUsageText));
expect(stderrText,
withUsage ? hasVerboseHelpMessage : isNot(hasVerboseHelpMessage));
return stderrText;
}
Future<String> assertParseArgsFailure(List<String> args) async {
var cli = _createCli();
await cli.run(args);
var stderrText = assertErrorExit(cli);
expect(stderrText, isNot(contains('Exception')));
return stderrText;
}
CommandLineOptions assertParseArgsSuccess(List<String> args) {
var cli = _createCli();
cli.parseCommandLineArgs(args);
expect(cli.exitCode, isNull);
var options = cli.options;
return options;
}
Future assertPreviewServerResponsive(String url) async {
var response = await http.get(url);
expect(response.statusCode, 200);
}
void assertProjectContents(String projectDir, Map<String, String> expected) {
for (var entry in expected.entries) {
var relativePathPosix = entry.key;
assert(!path.posix.isAbsolute(relativePathPosix));
var filePath = resourceProvider.pathContext
.join(projectDir, resourceProvider.convertPath(relativePathPosix));
expect(
resourceProvider.getFile(filePath).readAsStringSync(), entry.value);
}
}
String createProjectDir(Map<String, String> contents) {
var projectPathPosix = '/test_project';
for (var entry in contents.entries) {
var relativePathPosix = entry.key;
assert(!path.posix.isAbsolute(relativePathPosix));
var filePathPosix = path.posix.join(projectPathPosix, relativePathPosix);
resourceProvider.newFile(
resourceProvider.convertPath(filePathPosix), entry.value);
}
return resourceProvider.convertPath(projectPathPosix);
}
Future<void> runWithPreviewServer(_MigrationCli cli, List<String> args,
Future<void> Function(String) callback) async {
String url;
await cli.runWithPreviewServer(args, () async {
// Server should be running now
url = RegExp('http://.*', multiLine: true)
.stringMatch(logger.stdoutBuffer.toString());
await callback(url);
});
// Server should be stopped now
expect(http.get(url), throwsA(anything));
}
Map<String, String> simpleProject({bool migrated: false, String sourceText}) {
return {
'pubspec.yaml': '''
name: test
environment:
sdk: '${migrated ? '>=2.9.0 <2.10.0' : '>=2.6.0 <3.0.0'}'
''',
'lib/test.dart': sourceText ??
'''
int${migrated ? '?' : ''} f() => null;
'''
};
}
void tearDown() {
NonNullableFix.shutdownAllServers();
}
test_default_logger() {
// When running normally, we don't override the logger; make sure it has a
// non-null default so that there won't be a crash.
expect(MigrationCli(binaryName: 'nnbd_migration').logger, isNotNull);
}
test_detect_old_sdk() async {
var cli = _createCli();
// Alter the mock SDK, changing the signature of Object.operator== to match
// the signature that was present prior to NNBD. (This is what the
// migration tool uses to detect an old SDK).
var coreLib = resourceProvider.getFile(
resourceProvider.convertPath('${mock_sdk.sdkRoot}/lib/core/core.dart'));
var oldCoreLibText = coreLib.readAsStringSync();
var newCoreLibText = oldCoreLibText.replaceAll(
'external bool operator ==(Object other)',
'external bool operator ==(dynamic other)');
expect(newCoreLibText, isNot(oldCoreLibText));
coreLib.writeAsStringSync(newCoreLibText);
var projectDir = await createProjectDir(simpleProject());
await cli.run([projectDir]);
assertErrorExit(cli, withUsage: false);
var output = logger.stdoutBuffer.toString();
expect(
output,
contains(
'Bad state: Analysis seems to have an SDK without NNBD enabled'));
}
test_flag_apply_changes_default() {
expect(assertParseArgsSuccess([]).applyChanges, isFalse);
}
test_flag_apply_changes_disable() async {
// "--no-apply-changes" is not an option.
await assertParseArgsFailure(['--no-apply-changes']);
}
test_flag_apply_changes_enable() {
expect(
assertParseArgsSuccess(['--no-web-preview', '--apply-changes'])
.applyChanges,
isTrue);
}
test_flag_apply_changes_incompatible_with_web_preview() async {
expect(await assertParseArgsFailure(['--web-preview', '--apply-changes']),
contains('--apply-changes requires --no-web-preview'));
}
test_flag_help() async {
var helpText = await _getHelpText(verbose: false);
expect(helpText, hasUsageText);
expect(helpText, hasVerboseHelpMessage);
}
test_flag_help_verbose() async {
var helpText = await _getHelpText(verbose: true);
expect(helpText, hasUsageText);
expect(helpText, isNot(hasVerboseHelpMessage));
}
test_flag_ignore_errors_default() {
expect(assertParseArgsSuccess([]).ignoreErrors, isFalse);
}
test_flag_ignore_errors_disable() async {
await assertParseArgsFailure(['--no-ignore-errors']);
}
test_flag_ignore_errors_enable() {
expect(assertParseArgsSuccess(['--ignore-errors']).ignoreErrors, isTrue);
}
test_flag_web_preview_default() {
expect(assertParseArgsSuccess([]).webPreview, isTrue);
}
test_flag_web_preview_disable() {
expect(assertParseArgsSuccess(['--no-web-preview']).webPreview, isFalse);
}
test_flag_web_preview_enable() {
expect(assertParseArgsSuccess(['--web-preview']).webPreview, isTrue);
}
test_lifecycle_apply_changes() async {
var projectContents = simpleProject();
var projectDir = await createProjectDir(projectContents);
var cli = _createCli();
await cli.run(['--no-web-preview', '--apply-changes', projectDir]);
// Check that a summary was printed
expect(logger.stdoutBuffer.toString(), contains('Applying changes'));
// And that it refers to test.dart and pubspec.yaml
expect(logger.stdoutBuffer.toString(), contains('test.dart'));
expect(logger.stdoutBuffer.toString(), contains('pubspec.yaml'));
// And that it does not tell the user they can rerun with `--apply-changes`
expect(logger.stdoutBuffer.toString(), isNot(contains('--apply-changes')));
// Changes should have been made
assertProjectContents(projectDir, simpleProject(migrated: true));
}
test_lifecycle_ignore_errors_disable() async {
var projectContents = simpleProject(sourceText: '''
int f() => null
''');
var projectDir = await createProjectDir(projectContents);
var cli = _createCli();
await cli.run([projectDir]);
assertErrorExit(cli, withUsage: false);
var output = logger.stdoutBuffer.toString();
expect(output, contains('1 analysis issue found'));
var sep = resourceProvider.pathContext.separator;
expect(
output,
contains("error • Expected to find ';' at lib${sep}test.dart:1:12 • "
"(expected_token)"));
expect(
output,
contains(
'analysis errors will result in erroneous migration suggestions'));
expect(output, contains('Please fix the analysis issues'));
}
test_lifecycle_ignore_errors_enable() async {
var projectContents = simpleProject(sourceText: '''
int? f() => null
''');
var projectDir = await createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, ['--ignore-errors', projectDir],
(url) async {
var output = logger.stdoutBuffer.toString();
expect(output, isNot(contains('No analysis issues found')));
expect(
output,
contains('Continuing with migration suggestions due to the use of '
'--ignore-errors.'));
await assertPreviewServerResponsive(url);
});
}
test_lifecycle_no_preview() async {
var projectContents = simpleProject();
var projectDir = await createProjectDir(projectContents);
var cli = _createCli();
await cli.run(['--no-web-preview', projectDir]);
// Check that a summary was printed
expect(logger.stdoutBuffer.toString(), contains('Summary'));
// And that it refers to test.dart and pubspec.yaml
expect(logger.stdoutBuffer.toString(), contains('test.dart'));
expect(logger.stdoutBuffer.toString(), contains('pubspec.yaml'));
// And that it tells the user they can rerun with `--apply-changes`
expect(logger.stdoutBuffer.toString(), contains('--apply-changes'));
// No changes should have been made
assertProjectContents(projectDir, projectContents);
}
test_lifecycle_preview() async {
var projectContents = simpleProject();
var projectDir = await createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url);
});
// No changes should have been made.
assertProjectContents(projectDir, projectContents);
}
test_lifecycle_preview_extra_forward_slash() async {
var projectDir = await createProjectDir(simpleProject());
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
var uri = Uri.parse(url);
await assertPreviewServerResponsive(
uri.replace(path: uri.path + '/').toString());
});
}
test_lifecycle_uri_error() async {
var projectContents = simpleProject(sourceText: '''
import 'package:does_not/exist.dart';
int f() => null;
''');
var projectDir = await createProjectDir(projectContents);
var cli = _createCli();
await cli.run([projectDir]);
assertErrorExit(cli, withUsage: false);
var output = logger.stdoutBuffer.toString();
expect(output, contains('1 analysis issue found'));
expect(output, contains('uri_does_not_exist'));
expect(
output,
contains(
'analysis errors will result in erroneous migration suggestions'));
expect(output,
contains('Unresolved URIs found. Did you forget to run "pub get"?'));
expect(output, contains('Please fix the analysis issues'));
}
test_migrate_path_none() {
expect(assertParseArgsSuccess([]).directory, Directory.current.path);
}
test_migrate_path_one() {
expect(assertParseArgsSuccess(['foo']).directory, 'foo');
}
test_migrate_path_two() async {
var cli = _createCli();
await cli.run(['foo', 'bar']);
var stderrText = assertErrorExit(cli);
expect(stderrText, contains('No more than one path may be specified'));
}
test_option_preview_port() {
expect(
assertParseArgsSuccess(['--preview-port', '4040']).previewPort, 4040);
}
test_option_preview_port_default() {
expect(assertParseArgsSuccess([]).previewPort, isNull);
}
test_option_preview_port_format_error() async {
expect(await assertParseArgsFailure(['--preview-port', 'abc']),
contains('Invalid value for --preview-port'));
}
test_option_sdk() {
var path = Uri.parse('file:///foo/bar/baz').toFilePath();
expect(assertParseArgsSuccess(['--sdk-path', path]).sdkPath, same(path));
}
test_option_sdk_default() {
var cli = MigrationCli(binaryName: 'nnbd_migration');
cli.parseCommandLineArgs([]);
expect(
File(path.join(cli.options.sdkPath, 'version')).existsSync(), isTrue);
}
test_option_sdk_hidden() async {
var optionName = '--sdk-path';
expect(await _getHelpText(verbose: false), isNot(contains(optionName)));
expect(await _getHelpText(verbose: true), contains(optionName));
}
test_option_unrecognized() async {
expect(
await assertParseArgsFailure(['--this-option-does-not-exist']),
contains(
'Could not find an option named "this-option-does-not-exist"'));
}
test_uses_physical_resource_provider_by_default() {
var cli = MigrationCli(binaryName: 'nnbd_migration');
expect(cli.resourceProvider, same(PhysicalResourceProvider.INSTANCE));
}
_MigrationCli _createCli() {
mock_sdk.MockSdk(resourceProvider: resourceProvider);
return _MigrationCli(this);
}
Future<String> _getHelpText({@required bool verbose}) async {
var cli = _createCli();
await cli
.run(['--${CommandLineOptions.helpFlag}', if (verbose) '--verbose']);
expect(cli.exitCode, 0);
var helpText = logger.stderrBuffer.toString();
return helpText;
}
}
@reflectiveTest
class _MigrationCliTestPosix extends _MigrationCliTestBase
with _MigrationCliTestMethods {
@override
final resourceProvider;
_MigrationCliTestPosix()
: resourceProvider = MemoryResourceProvider(
context: path.style == path.Style.posix ? null : path.posix);
}
@reflectiveTest
class _MigrationCliTestWindows extends _MigrationCliTestBase
with _MigrationCliTestMethods {
@override
final resourceProvider;
_MigrationCliTestWindows()
: resourceProvider = MemoryResourceProvider(
context: path.style == path.Style.windows
? null
: path.Context(style: path.Style.windows, current: 'C:\\'));
}
/// TODO(paulberry): move into cli_util
class _TestLogger implements Logger {
final stderrBuffer = StringBuffer();
final stdoutBuffer = StringBuffer();
final bool isVerbose;
_TestLogger(this.isVerbose);
@override
Ansi get ansi => Ansi(false);
@override
void flush() {
throw UnimplementedError('TODO(paulberry)');
}
@override
Progress progress(String message) {
return SimpleProgress(this, message);
}
@override
void stderr(String message) {
stderrBuffer.writeln(message);
}
@override
void stdout(String message) {
stdoutBuffer.writeln(message);
}
@override
void trace(String message) {
throw UnimplementedError('TODO(paulberry)');
}
}