blob: 6384be9a7fe40617867ab7221b212f662114085b [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:convert';
import 'dart:io';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart' show ResourceProvider;
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart' as mock_sdk;
import 'package:args/args.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:http/http.dart' as http;
import 'package:nnbd_migration/instrumentation.dart';
import 'package:nnbd_migration/migration_cli.dart';
import 'package:nnbd_migration/src/front_end/dartfix_listener.dart';
import 'package:nnbd_migration/src/front_end/instrumentation_listener.dart';
import 'package:nnbd_migration/src/front_end/migration_summary.dart';
import 'package:nnbd_migration/src/front_end/non_nullable_fix.dart';
import 'package:nnbd_migration/src/front_end/web/edit_details.dart';
import 'package:nnbd_migration/src/front_end/web/file_details.dart';
import 'package:nnbd_migration/src/front_end/web/navigation_tree.dart';
import 'package:nnbd_migration/src/messages.dart' as messages;
import 'package:nnbd_migration/src/preview/preview_site.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'utilities/test_logger.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(_MigrationCliTestPosix);
defineReflectiveTests(_MigrationCliTestWindows);
});
}
const sdkRootPathPosix = '/sdk';
/// Specialization of [InstrumentationListener] that generates artificial
/// exceptions, so that we can test they are properly propagated to top level.
class _ExceptionGeneratingInstrumentationListener
extends InstrumentationListener {
_ExceptionGeneratingInstrumentationListener(
{MigrationSummary? migrationSummary})
: super(migrationSummary: migrationSummary);
@override
void externalDecoratedType(Element element, DecoratedTypeInfo decoratedType) {
if (element.name == 'print') {
throw StateError('Artificial exception triggered');
}
super.externalDecoratedType(element, decoratedType);
}
}
/// Specialization of [NonNullableFix] that generates artificial exceptions, so
/// that we can test they are properly propagated to top level.
class _ExceptionGeneratingNonNullableFix extends NonNullableFix {
_ExceptionGeneratingNonNullableFix(DartFixListener listener,
ResourceProvider resourceProvider, Object? bindAddress, Logger logger,
{List<String> included = const <String>[],
int? preferredPort,
String? summaryPath,
required String sdkPath})
: super(listener, resourceProvider, bindAddress, logger,
(String? path) => true,
included: included,
preferredPort: preferredPort,
summaryPath: summaryPath,
sdkPath: sdkPath);
@override
InstrumentationListener createInstrumentationListener(
{MigrationSummary? migrationSummary}) =>
_ExceptionGeneratingInstrumentationListener(
migrationSummary: migrationSummary);
}
class _MigrationCli extends MigrationCli {
final _MigrationCliTestBase _test;
/// If non-null, callback function that will be invoked by the `applyHook`
/// override.
void Function()? _onApplyHook;
_MigrationCli(this._test)
: super(
binaryName: 'nnbd_migration',
loggerFactory: (isVerbose) => _test.logger = TestLogger(isVerbose),
defaultSdkPathOverride:
_test.resourceProvider.convertPath(sdkRootPathPosix),
resourceProvider: _test.resourceProvider,
environmentVariables: _test.environmentVariables);
_MigrationCliRunner? decodeCommandLineArgs(ArgResults argResults,
{bool? isVerbose}) {
var runner = super.decodeCommandLineArgs(argResults, isVerbose: isVerbose);
if (runner == null) return null;
return _MigrationCliRunner(this, runner.options);
}
}
class _MigrationCliRunner extends MigrationCliRunner {
Future<void> Function()? _runWhilePreviewServerActive;
_MigrationCliRunner(_MigrationCli cli, CommandLineOptions options)
: super(cli, options);
_MigrationCli get cli => super.cli as _MigrationCli;
@override
void applyHook() {
super.applyHook();
cli._onApplyHook?.call();
}
@override
Object? computeBindAddress() {
var address = super.computeBindAddress();
if (Platform.environment.containsKey('FORCE_IPV6') &&
address == InternetAddress.loopbackIPv4) {
return InternetAddress.loopbackIPv6;
}
return address;
}
@override
Set<String> computePathsToProcess(DriverBasedAnalysisContext context) =>
cli._test.overridePathsToProcess ??
_sortPaths(super.computePathsToProcess(context));
@override
NonNullableFix createNonNullableFix(DartFixListener listener,
ResourceProvider resourceProvider, Object? bindAddress,
{List<String> included = const <String>[],
int? preferredPort,
String? summaryPath,
required String sdkPath}) {
if (cli._test.injectArtificialException) {
return _ExceptionGeneratingNonNullableFix(
listener, resourceProvider, bindAddress, logger,
included: included,
preferredPort: preferredPort,
summaryPath: summaryPath,
sdkPath: sdkPath);
} else {
return super.createNonNullableFix(listener, resourceProvider, bindAddress,
included: included,
preferredPort: preferredPort,
summaryPath: summaryPath,
sdkPath: sdkPath);
}
}
@override
void listenForSignalInterrupt() {
if (_runWhilePreviewServerActive == null) {
fail('Preview server not expected to have been started');
}
sigIntSignalled = Completer();
_runWhilePreviewServerActive!
.call()
.then((_) => sigIntSignalled.complete());
_runWhilePreviewServerActive = null;
}
Future<void> runWithPreviewServer(Future<void> Function() callback) async {
_runWhilePreviewServerActive = callback;
await run();
if (_runWhilePreviewServerActive != null) {
fail('Preview server never started');
}
}
@override
bool shouldBeMigrated(String path) =>
cli._test.overrideShouldBeMigrated?.call(path) ??
super.shouldBeMigrated(path);
/// Sorts the paths in [paths] for repeatability of migration tests.
Set<String> _sortPaths(Set<String> paths) {
var pathList = paths.toList();
pathList.sort();
return pathList.toSet();
}
}
abstract class _MigrationCliTestBase {
Map<String, String> environmentVariables = {};
/// If `true`, then an artificial exception should be generated when migration
/// encounters a reference to the `print` function.
bool injectArtificialException = false;
/// If non-null, this is injected as the return value for
/// [_MigrationCliRunner.computePathsToProcess].
Set<String>? overridePathsToProcess;
bool Function(String)? overrideShouldBeMigrated;
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');
final urlStartRegexp = RegExp('https?:');
final dartVersionIsNullSafeByDefault =
Feature.non_nullable.releaseVersion != null;
String assertDecodeArgsFailure(List<String> args) {
var cli = _createCli();
try {
cli.decodeCommandLineArgs(MigrationCli.createParser().parse(args));
fail('Migration succeeded; expected it to abort with an error');
} on MigrationExit catch (migrationExit) {
expect(migrationExit.exitCode, isNotNull);
expect(migrationExit.exitCode, isNot(0));
}
var stderrText = logger.stderrBuffer.toString();
expect(stderrText, hasUsageText);
expect(stderrText, hasVerboseHelpMessage);
return stderrText;
}
Future<String> assertErrorExit(
MigrationCliRunner cliRunner, FutureOr<void> Function() callback,
{required bool withUsage, dynamic expectedExitCode}) async {
expectedExitCode ??= isNot(0);
try {
await callback();
fail('Migration succeeded; expected it to abort with an error');
} on MigrationExit catch (migrationExit) {
expect(migrationExit.exitCode, isNotNull);
expect(migrationExit.exitCode, expectedExitCode);
}
expect(cliRunner.isPreviewServerRunning, isFalse);
return assertStderr(withUsage: withUsage);
}
void assertHttpSuccess(http.Response response) {
if (response.statusCode == 500) {
try {
var decodedResponse = jsonDecode(response.body);
print('Exception: ${decodedResponse['exception']}');
print('Stack trace:');
print(decodedResponse['stackTrace']);
} catch (_) {
print(response.body);
}
fail('HTTP request failed');
}
expect(response.statusCode, 200);
}
void assertNormalExit(MigrationCliRunner cliRunner) {
expect(cliRunner.isPreviewServerRunning, isFalse);
}
Future<String> assertParseArgsFailure(List<String> args) async {
try {
MigrationCli.createParser().parse(args);
} on FormatException catch (e) {
// Parsing failed, which was expected.
return e.message;
}
fail('Parsing was expected to fail, but did not');
}
CommandLineOptions assertParseArgsSuccess(List<String> args) {
var cliRunner = _createCli()
.decodeCommandLineArgs(MigrationCli.createParser().parse(args))!;
assertNormalExit(cliRunner);
var options = cliRunner.options;
expect(options, isNotNull);
return options;
}
Future assertPreviewServerResponsive(String url) async {
var response = await httpGet(Uri.parse(url));
assertHttpSuccess(response);
}
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);
}
}
Future<String> assertRunFailure(List<String> args,
{MigrationCli? cli,
bool withUsage = false,
dynamic expectedExitCode}) async {
expectedExitCode ??= isNot(0);
cli ??= _createCli();
MigrationCliRunner? cliRunner;
try {
cliRunner =
cli.decodeCommandLineArgs(MigrationCli.createParser().parse(args));
} on MigrationExit catch (e) {
expect(e.exitCode, isNotNull);
expect(e.exitCode, expectedExitCode);
return assertStderr(withUsage: withUsage);
}
return await assertErrorExit(cliRunner!, () => cliRunner!.run(),
withUsage: withUsage, expectedExitCode: expectedExitCode);
}
String assertStderr({required bool withUsage}) {
var stderrText = logger.stderrBuffer.toString();
expect(stderrText, withUsage ? hasUsageText : isNot(hasUsageText));
expect(stderrText,
withUsage ? hasVerboseHelpMessage : isNot(hasVerboseHelpMessage));
return stderrText;
}
/// Wraps a future containing an HTTP response so that when that response is
/// received, we will verify that it is reasonable.
Future<http.Response> checkHttpResponse(
Future<http.Response> futureResponse) async {
var response = await futureResponse;
// Check that all "http:" and "https:" URLs in the given HTTP response are
// absolute (guards against https://github.com/dart-lang/sdk/issues/43545).
for (var match in urlStartRegexp.allMatches(response.body)) {
expect(response.body.substring(match.end), startsWith('//'));
}
return response;
}
String createProjectDir(Map<String, String?> contents,
{String posixPath = '/test_project'}) {
for (var entry in contents.entries) {
var relativePathPosix = entry.key;
assert(!path.posix.isAbsolute(relativePathPosix));
var filePathPosix = path.posix.join(posixPath, relativePathPosix);
resourceProvider.newFile(
resourceProvider.convertPath(filePathPosix), entry.value!);
}
return resourceProvider.convertPath(posixPath);
}
Future<String?> getSourceFromServer(Uri uri, String path) async {
http.Response response = await tryGetSourceFromServer(uri, path);
assertHttpSuccess(response);
return jsonDecode(response.body)['sourceCode'] as String?;
}
/// Performs an HTTP get, verifying that the response received (if any) is
/// reasonable.
Future<http.Response> httpGet(Uri url, {Map<String, String>? headers}) {
return checkHttpResponse(http.get(url, headers: headers));
}
/// Performs an HTTP post, verifying that the response received (if any) is
/// reasonable.
Future<http.Response> httpPost(Uri url,
{Map<String, String>? headers, dynamic body, Encoding? encoding}) {
return checkHttpResponse(
http.post(url, headers: headers, body: body, encoding: encoding));
}
String packagePath(String path) =>
resourceProvider.convertPath('/.pub-cache/$path');
Future<void> runWithPreviewServer(_MigrationCli cli, List<String> args,
Future<void> Function(String?) callback) async {
String? url;
var cliRunner = cli.decodeCommandLineArgs(_parseArgs(args));
if (cliRunner != null) {
await cliRunner.runWithPreviewServer(() async {
// Server should be running now
url = RegExp('http://.*', multiLine: true)
.stringMatch(logger.stdoutBuffer.toString());
await callback(url);
});
// Server should be stopped now
expect(httpGet(Uri.parse(url!)), throwsA(anything));
assertNormalExit(cliRunner);
}
}
void setUp() {
resourceProvider.newFolder(resourceProvider.pathContext.current);
environmentVariables.clear();
}
Map<String, String?> simpleProject(
{bool migrated = false,
String? sourceText,
String? pubspecText,
String? packageConfigText,
String? analysisOptionsText}) {
return {
'pubspec.yaml': pubspecText ??
'''
name: test
environment:
sdk: '${migrated ? '>=2.12.0 <3.0.0' : '>=2.6.0 <3.0.0'}'
''',
'.dart_tool/package_config.json':
packageConfigText ?? _getPackageConfigText(migrated: migrated),
'lib/test.dart': sourceText ??
'''
int${migrated ? '?' : ''} f() => null;
''',
if (analysisOptionsText != null)
'analysis_options.yaml': analysisOptionsText,
};
}
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('$sdkRootPathPosix/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 = createProjectDir(simpleProject());
await assertRunFailure([projectDir], cli: cli);
var output = logger.stdoutBuffer.toString();
expect(output, contains(messages.sdkNnbdOff));
}
test_detect_old_sdk_environment_variable() async {
environmentVariables['SDK_PATH'] = '/fake-old-sdk-path';
var cli = _createCli(); // Creates the mock SDK as a side effect
// 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('$sdkRootPathPosix/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 = createProjectDir(simpleProject());
await assertRunFailure([projectDir], cli: cli);
var output = logger.stdoutBuffer.toString();
expect(output, contains(messages.sdkNnbdOff));
expect(output, contains(messages.sdkPathEnvironmentVariableSet));
expect(output, contains(environmentVariables['SDK_PATH']));
}
test_flag_apply_changes_default() {
expect(assertParseArgsSuccess([]).applyChanges, isFalse);
expect(assertParseArgsSuccess([]).webPreview, isTrue);
}
test_flag_apply_changes_disable() async {
// "--no-apply-changes" is not an option.
await assertParseArgsFailure(['--no-apply-changes']);
}
test_flag_apply_changes_enable() {
var options = assertParseArgsSuccess(['--apply-changes']);
expect(options.applyChanges, isTrue);
expect(options.webPreview, isFalse);
}
test_flag_apply_changes_enable_with_no_web_preview() {
expect(
assertParseArgsSuccess(['--no-web-preview', '--apply-changes'])
.applyChanges,
isTrue);
}
test_flag_apply_changes_incompatible_with_web_preview() {
expect(assertDecodeArgsFailure(['--web-preview', '--apply-changes']),
contains('--apply-changes requires --no-web-preview'));
}
test_flag_help() {
var helpText = _getHelpText(verbose: false);
expect(helpText, hasUsageText);
expect(helpText, hasVerboseHelpMessage);
}
test_flag_help_verbose() {
var helpText = _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_ignore_exceptions_default() {
expect(assertParseArgsSuccess([]).ignoreExceptions, isFalse);
}
test_flag_ignore_exceptions_disable() async {
await assertParseArgsFailure(['--no-ignore-exceptions']);
}
test_flag_ignore_exceptions_enable() {
expect(assertParseArgsSuccess(['--ignore-exceptions']).ignoreExceptions,
isTrue);
}
test_flag_ignore_exceptions_hidden() {
var flagName = '--ignore-exceptions';
expect(_getHelpText(verbose: false), isNot(contains(flagName)));
expect(_getHelpText(verbose: true), contains(flagName));
}
test_flag_skip_import_check_default() {
expect(assertParseArgsSuccess([]).skipImportCheck, isFalse);
}
test_flag_skip_import_check_disable() async {
// "--no-skip-import-check" is not an option.
await assertParseArgsFailure(['--no-skip-import_check']);
}
test_flag_skip_import_check_enable() {
expect(assertParseArgsSuccess(['--skip-import-check']).skipImportCheck,
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_already_migrated_file() async {
Map<String, String?> createProject({bool migrated = false}) {
var projectContents = simpleProject(sourceText: '''
${migrated ? '' : '// @dart = 2.6'}
import 'already_migrated.dart';
int${migrated ? '?' : ''} x = y;
''', migrated: true);
projectContents['lib/already_migrated.dart'] = '''
int? y = 0;
''';
return projectContents;
}
var projectContents = createProject();
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
assertNormalExit(cliRunner);
// Check that a summary was printed
expect(logger.stdoutBuffer.toString(), contains('Applying changes'));
// And that it refers to test.dart, but not pubspec.yaml or
// already_migrated.dart.
expect(logger.stdoutBuffer.toString(), contains('test.dart'));
expect(logger.stdoutBuffer.toString(), isNot(contains('pubspec.yaml')));
expect(logger.stdoutBuffer.toString(),
isNot(contains('already_migrated.dart')));
// And that it does not tell the user they can rerun with `--apply-changes`
expect(logger.stdoutBuffer.toString(), isNot(contains('--apply-changes')));
// Check that the non-migrated library was changed but not the migrated one
assertProjectContents(projectDir, createProject(migrated: true));
}
test_lifecycle_apply_changes() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
var cliRunner =
cli.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
bool applyHookCalled = false;
cli._onApplyHook = () {
expect(applyHookCalled, false);
applyHookCalled = true;
// Changes should have been made
assertProjectContents(projectDir, simpleProject(migrated: true));
};
await cliRunner.run();
assertNormalExit(cliRunner);
expect(applyHookCalled, true);
// 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')));
}
test_lifecycle_contextdiscovery_handles_multiple() async {
var projectContents = simpleProject();
var subProject = simpleProject();
for (var filePath in subProject.keys) {
projectContents['example/$filePath'] = subProject[filePath];
}
projectContents['example/analysis_options.yaml'] = '''
analyzer:
strong-mode:
implicit-casts: false
linter:
rules:
- empty_constructor_bodies
''';
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--no-web-preview', projectDir]))!;
await cliRunner.run();
assertNormalExit(cliRunner);
expect(cliRunner.hasMultipleAnalysisContext, true);
expect(cliRunner.analysisContext, isNotNull);
var output = logger.stdoutBuffer.toString();
expect(output, contains('more than one project found'));
}
test_lifecycle_contextdiscovery_handles_single() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--no-web-preview', projectDir]))!;
await cliRunner.run();
assertNormalExit(cliRunner);
expect(cliRunner.hasMultipleAnalysisContext, false);
expect(cliRunner.analysisContext, isNotNull);
}
test_lifecycle_exception_handling() async {
var projectContents = simpleProject(sourceText: 'main() { print(0); }');
var projectDir = createProjectDir(projectContents);
injectArtificialException = true;
await assertRunFailure([projectDir]);
var errorOutput = logger.stderrBuffer.toString();
expect(errorOutput, contains('Artificial exception triggered'));
expect(
errorOutput, isNot(contains('try to fix errors in the source code')));
expect(errorOutput, contains('re-run with\n--ignore-exceptions'));
expect(errorOutput, contains('consider filing a bug report'));
expect(
errorOutput,
contains(
RegExp(r'Please include the SDK version \([0-9]+\.[0-9]+\..*\)')));
}
test_lifecycle_exception_handling_ignore() async {
var projectContents = simpleProject(sourceText: '''
main() {
print(0);
int x = null;
}
''');
var projectDir = createProjectDir(projectContents);
injectArtificialException = true;
var cli = _createCli();
await runWithPreviewServer(cli, ['--ignore-exceptions', projectDir],
(url) async {
var output = logger.stdoutBuffer.toString();
expect(output, contains('No analysis issues found'));
expect(output, isNot(contains('Artificial exception triggered')));
expect(
output,
contains('Attempting to perform\nmigration anyway due to the use'
' of --ignore-exceptions.'));
expect(output, contains('re-run without --ignore-exceptions'));
await assertPreviewServerResponsive(url!);
await _tellPreviewToApplyChanges(url);
assertProjectContents(
projectDir, simpleProject(migrated: true, sourceText: '''
main() {
print(0);
int? x = null;
}
'''));
});
expect(logger.stderrBuffer.toString(), isEmpty);
}
test_lifecycle_exception_handling_multiple() async {
var projectContents =
simpleProject(sourceText: 'main() { print(0); print(1); }');
var projectDir = createProjectDir(projectContents);
injectArtificialException = true;
await assertRunFailure([projectDir]);
var errorOutput = logger.stderrBuffer.toString();
expect(
'Artificial exception triggered'.allMatches(errorOutput), hasLength(1));
expect(
errorOutput, isNot(contains('try to fix errors in the source code')));
expect(errorOutput, contains('re-run with\n--ignore-exceptions'));
}
test_lifecycle_exception_handling_with_error() async {
var projectContents =
simpleProject(sourceText: 'main() { print(0); unresolved; }');
var projectDir = createProjectDir(projectContents);
injectArtificialException = true;
await assertRunFailure(['--ignore-errors', projectDir]);
var errorOutput = logger.stderrBuffer.toString();
expect(errorOutput, contains('Artificial exception triggered'));
expect(errorOutput, contains('try to fix errors in the source code'));
expect(errorOutput, contains('re-run with\n--ignore-exceptions'));
}
test_lifecycle_ignore_errors_disable() async {
var projectContents = simpleProject(sourceText: '''
int f() => null
''');
var projectDir = createProjectDir(projectContents);
await assertRunFailure([projectDir]);
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('erroneous migration suggestions'));
expect(output, contains('We recommend fixing the analysis issues'));
expect(output, isNot(contains('Set the lower SDK constraint')));
}
test_lifecycle_ignore_errors_enable() async {
var projectContents = simpleProject(sourceText: '''
int? f() => null
''');
var projectDir = 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_import_check_via_export() async {
// If the user's code exports a library that imports a non-migrated library,
// that's a problem too.
var projectContents = simpleProject(
sourceText: "export 'package:foo/foo.dart';",
packageConfigText: _getPackageConfigText(
migrated: false, packagesMigrated: {'foo': true, 'bar': false}));
var projectDir = createProjectDir(projectContents);
resourceProvider.newFile(
packagePath('foo/lib/foo.dart'), "import 'package:bar/bar.dart';");
resourceProvider.newFile(packagePath('bar/lib/bar.dart'), '');
await assertRunFailure([projectDir], expectedExitCode: 1);
var output = logger.stdoutBuffer.toString();
expect(output, contains('Error: package has unmigrated dependencies'));
// Output should mention bar.dart, since it's unmigrated
expect(output, contains('package:bar/bar.dart'));
// But it should not mention foo.dart, which is migrated
expect(output, isNot(contains('package:foo/foo.dart')));
}
test_lifecycle_import_check_via_indirect_export() async {
// If the user's code imports a library that exports a library that imports
// a non-migrated library, that's a problem too.
var projectContents = simpleProject(
sourceText: "import 'package:foo/foo.dart';",
packageConfigText: _getPackageConfigText(
migrated: false,
packagesMigrated: {'foo': true, 'bar': true, 'baz': false}));
var projectDir = createProjectDir(projectContents);
resourceProvider.newFile(
packagePath('foo/lib/foo.dart'), "export 'package:bar/bar.dart';");
resourceProvider.newFile(
packagePath('bar/lib/bar.dart'), "import 'package:baz/baz.dart';");
resourceProvider.newFile(packagePath('baz/lib/baz.dart'), '');
await assertRunFailure([projectDir], expectedExitCode: 1);
var output = logger.stdoutBuffer.toString();
expect(output, contains('Error: package has unmigrated dependencies'));
// Output should mention baz.dart, since it's unmigrated
expect(output, contains('package:baz/baz.dart'));
// But it should not mention foo.dart or bar.dart, which are migrated
expect(output, isNot(contains('package:foo/foo.dart')));
expect(output, isNot(contains('package:bar/bar.dart')));
}
test_lifecycle_import_lib_from_test() async {
Map<String, String?> makeProject({bool migrated = false}) {
return simpleProject(migrated: migrated)
..['test/foo.dart'] = '''
import '../lib/test.dart';
''';
}
var projectContents = makeProject();
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
var cliRunner =
cli.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
bool applyHookCalled = false;
cli._onApplyHook = () {
expect(applyHookCalled, false);
applyHookCalled = true;
// Changes should have been made
assertProjectContents(projectDir, makeProject(migrated: true));
};
await cliRunner.run();
assertNormalExit(cliRunner);
expect(applyHookCalled, true);
}
@FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/44118')
test_lifecycle_issue_44118() async {
var projectContents = simpleProject(sourceText: '''
int f() => null
''');
projectContents['lib/foo.dart'] = '''
import 'test.dart';
''';
var projectDir = createProjectDir(projectContents);
await assertRunFailure([projectDir]);
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_migration_already_performed() async {
var projectContents = simpleProject(migrated: true);
var projectDir = createProjectDir(projectContents);
await assertRunFailure([projectDir], expectedExitCode: 0);
var output = logger.stdoutBuffer.toString();
expect(output,
contains('All sources appear to be already migrated. Nothing to do.'));
}
test_lifecycle_no_preview() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--no-web-preview', projectDir]))!;
await cliRunner.run();
assertNormalExit(cliRunner);
// Check that a summary was printed
var output = logger.stdoutBuffer.toString();
expect(output, contains('Diff of changes'));
// And that it refers to test.dart and pubspec.yaml
expect(output, contains('test.dart'));
expect(output, contains('pubspec.yaml'));
// And that it contains text from a changed line
expect(output, contains('f() => null'));
// And that it tells the user they can rerun with `--apply-changes`
expect(output, contains('--apply-changes'));
// No changes should have been made
assertProjectContents(projectDir, projectContents);
}
test_lifecycle_override_paths() async {
Map<String, String?> makeProject({bool migrated = false}) {
var projectContents = simpleProject(migrated: migrated);
projectContents['lib/test.dart'] = '''
import 'skip.dart';
import 'analyze_but_do_not_migrate.dart';
void f(int x) {}
void g(int${migrated ? '?' : ''} x) {}
void h(int${migrated ? '?' : ''} x) {}
void call_h() => h(null);
''';
projectContents['lib/skip.dart'] = '''
import 'test.dart';
void call_f() => f(null);
''';
projectContents['lib/analyze_but_do_not_migrate.dart'] = '''
import 'test.dart';
void call_g() => g(null);
''';
return projectContents;
}
var projectContents = makeProject();
var projectDir = createProjectDir(projectContents);
var testPath =
resourceProvider.pathContext.join(projectDir, 'lib', 'test.dart');
var analyzeButDoNotMigratePath = resourceProvider.pathContext
.join(projectDir, 'lib', 'analyze_but_do_not_migrate.dart');
overridePathsToProcess = {testPath, analyzeButDoNotMigratePath};
overrideShouldBeMigrated = (path) => path == testPath;
var cliRunner = _createCli().decodeCommandLineArgs(_parseArgs([
'--no-web-preview',
'--apply-changes',
'--skip-import-check',
projectDir
]))!;
await cliRunner.run();
assertNormalExit(cliRunner);
// 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 only to test.dart, and only accounting for
// the calls coming from analyze_but_do_not_migrate.dart and test.dart
assertProjectContents(projectDir, makeProject(migrated: true));
}
test_lifecycle_preview() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
var localhostAddressText = Platform.environment.containsKey('FORCE_IPV6')
? '[::1]'
: '127.0.0.1';
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
expect(url, startsWith('http://$localhostAddressText:'));
await assertPreviewServerResponsive(url!);
});
// No changes should have been made.
assertProjectContents(projectDir, projectContents);
}
test_lifecycle_preview_add_hint() async {
var projectContents = simpleProject(sourceText: 'int x;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var authToken = uri.queryParameters['authToken'];
var response = await httpPost(
uri.replace(
path: resourceProvider.pathContext
.toUri(resourceProvider.pathContext
.join(projectDir, 'lib', 'test.dart'))
.path,
queryParameters: {
'offset': '3',
'end': '3',
'replacement': '/*!*/',
'authToken': authToken
}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
assertProjectContents(
projectDir, simpleProject(sourceText: 'int/*!*/ x;'));
});
}
test_lifecycle_preview_apply_changes() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
bool applyHookCalled = false;
cli._onApplyHook = () {
expect(applyHookCalled, false);
applyHookCalled = true;
// Changes should have been made
assertProjectContents(projectDir, simpleProject(migrated: true));
};
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
await _tellPreviewToApplyChanges(url);
expect(applyHookCalled, true);
});
}
test_lifecycle_preview_apply_changes_unreferenced_part() async {
var projectContents = simpleProject()
..['lib/unreferenced_part.dart'] = '''
part of foo;
int f() => null;
''';
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
bool applyHookCalled = false;
cli._onApplyHook = () {
expect(applyHookCalled, false);
applyHookCalled = true;
// Changes should have been made
assertProjectContents(projectDir, simpleProject(migrated: true));
};
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
await _tellPreviewToApplyChanges(url);
expect(applyHookCalled, true);
});
}
test_lifecycle_preview_extra_forward_slash() async {
var projectDir = 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_preview_navigation_links() async {
var projectContents = simpleProject(sourceText: 'int x;');
projectContents['lib/src/test.dart'] = 'import "../test.dart"; int y = x;';
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
final uri = Uri.parse(url);
final authToken = uri.queryParameters['authToken'];
final fileResponse = await httpGet(
uri.replace(
path: resourceProvider.pathContext
.toUri(resourceProvider.pathContext
.join(projectDir, 'lib', 'src', 'test.dart'))
.path,
queryParameters: {'inline': 'true', 'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
final fileJson = FileDetails.fromJson(jsonDecode(fileResponse.body));
final navigation = fileJson.navigationContent!;
final aLink = RegExp(r'<a href="([^"]+)" class="nav-link">');
for (final match in aLink.allMatches(navigation)) {
var href = match.group(1)!;
final contentsResponse = await httpGet(
uri.replace(
path: Uri.parse(href).path,
queryParameters: {'inline': 'true', 'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(contentsResponse);
}
});
}
test_lifecycle_preview_navigation_tree() async {
var projectContents = simpleProject(sourceText: 'int x;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var authToken = uri.queryParameters['authToken'];
var treeResponse = await httpGet(
uri.replace(
path: '/_preview/navigationTree.json',
queryParameters: {'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
var navRoots = jsonDecode(treeResponse.body) as List<Object?>;
for (final root in navRoots) {
var navTree =
NavigationTreeNode.fromJson(root) as NavigationTreeDirectoryNode;
for (final file in navTree.subtree!) {
if (file is NavigationTreeFileNode) {
final contentsResponse = await httpGet(
uri
.resolve(file.href!)
.replace(queryParameters: {'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(contentsResponse);
}
}
}
});
}
test_lifecycle_preview_on_host_any() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cli = _createCli()
..decodeCommandLineArgs(_parseArgs(['--preview-hostname=any']));
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(url, isNot(contains('localhost')));
await assertPreviewServerResponsive(url!);
});
// No changes should have been made.
assertProjectContents(projectDir, projectContents);
}
test_lifecycle_preview_region_link() async {
var projectContents = simpleProject(sourceText: 'int x;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var authToken = uri.queryParameters['authToken'];
var regionResponse = await httpGet(
uri.replace(
path: resourceProvider.pathContext
.toUri(resourceProvider.pathContext
.join(projectDir, 'lib', 'test.dart'))
.path,
queryParameters: {
'region': 'region',
'offset': '3',
'authToken': authToken
}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
var regionJson = EditDetails.fromJson(jsonDecode(regionResponse.body));
final displayPath = regionJson.displayPath!;
final uriPath = regionJson.uriPath;
// uriPath should be a working URI
final contentsResponse = await httpGet(
uri.replace(
path: uriPath,
queryParameters: {'inline': 'true', 'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(contentsResponse);
// Display path should be the actual windows path
final file = resourceProvider
.getFolder(projectDir)
.getChildAssumingFile(displayPath);
expect(file.exists, isTrue);
});
}
test_lifecycle_preview_region_table_path() async {
var projectContents = simpleProject(sourceText: 'int x;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
final uri = Uri.parse(url);
final authToken = uri.queryParameters['authToken'];
final fileResponse = await httpGet(
uri.replace(
path: resourceProvider.pathContext
.toUri(resourceProvider.pathContext
.join(projectDir, 'lib', 'test.dart'))
.path,
queryParameters: {'inline': 'true', 'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
final fileJson = FileDetails.fromJson(jsonDecode(fileResponse.body));
final regions = fileJson.regions!;
final regionsPathRegex = RegExp(r'<table data-path="([^"]+)">');
expect(regionsPathRegex.hasMatch(regions), true);
final regionsPath = regionsPathRegex.matchAsPrefix(regions)!.group(1)!;
final contentsResponse = await httpGet(
uri.replace(
path: Uri.parse(regionsPath).path,
queryParameters: {'inline': 'true', 'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(contentsResponse);
});
}
test_lifecycle_preview_rerun() async {
var origSourceText = 'void f() {}';
var projectContents = simpleProject(sourceText: origSourceText);
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var testPath =
resourceProvider.pathContext.join(projectDir, 'lib', 'test.dart');
var newSourceText = 'void g() {}';
resourceProvider.getFile(testPath).writeAsStringSync(newSourceText);
// We haven't rerun, so getting the file details from the server should
// still yield the original source text
expect(await getSourceFromServer(uri, testPath), origSourceText);
var response = await httpPost(uri.replace(path: 'rerun-migration'),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
// Now that we've rerun, the server should yield the new source text
expect(await getSourceFromServer(uri, testPath), newSourceText);
});
}
test_lifecycle_preview_rerun_added_file() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var test2Path =
resourceProvider.pathContext.join(projectDir, 'lib', 'test2.dart');
var newSourceText = 'void g() {}';
resourceProvider.getFile(test2Path).writeAsStringSync(newSourceText);
// We haven't rerun, so getting the file details from the server should
// fail
var response = await tryGetSourceFromServer(uri, test2Path);
expect(response.statusCode, 404);
response = await httpPost(uri.replace(path: 'rerun-migration'),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
// Now that we've rerun, the server should yield the new source text
expect(await getSourceFromServer(uri, test2Path), newSourceText);
});
}
test_lifecycle_preview_rerun_deleted_file() async {
var projectContents = {...simpleProject(), 'lib/other.dart': ''};
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
// Note: we use the summary to verify that the deletion was noticed
var summaryPath = resourceProvider.convertPath('/summary.json');
await runWithPreviewServer(cli, ['--summary', summaryPath, projectDir],
(url) async {
await assertPreviewServerResponsive(url!);
// lib/test.dart should be readable from the server and appear in the
// summary
var uri = Uri.parse(url);
var testPath =
resourceProvider.pathContext.join(projectDir, 'lib', 'test.dart');
await getSourceFromServer(uri, testPath);
var summaryData =
jsonDecode(resourceProvider.getFile(summaryPath).readAsStringSync());
var separator = resourceProvider.pathContext.separator;
expect(summaryData['changes']['byPath'],
contains('lib${separator}test.dart'));
// Now delete the lib file and rerun
resourceProvider.deleteFile(testPath);
var response = await httpPost(uri.replace(path: 'rerun-migration'),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
// lib/test.dart should no longer be readable from the server and
// should no longer appear in the summary
response = await tryGetSourceFromServer(uri, testPath);
expect(response.statusCode, 404);
summaryData =
jsonDecode(resourceProvider.getFile(summaryPath).readAsStringSync());
expect(summaryData['changes']['byPath'],
isNot(contains('lib${separator}test.dart')));
});
}
test_lifecycle_preview_rerun_with_ignore_errors() async {
var origSourceText = 'void f(int i) {}';
var projectContents = simpleProject(sourceText: origSourceText);
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, ['--ignore-errors', projectDir],
(url) async {
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var testPath =
resourceProvider.pathContext.join(projectDir, 'lib', 'test.dart');
resourceProvider.getFile(testPath).writeAsStringSync('void f(int? i) {}');
// We haven't rerun, so getting the file details from the server should
// still yield the original source text, with informational space.
expect(await getSourceFromServer(uri, testPath), 'void f(int i) {}');
var response = await httpPost(uri.replace(path: 'rerun-migration'),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
var body = jsonDecode(response.body);
expect(body['success'], isTrue);
expect(body['errors'], isNull);
// Now that we've rerun, the server should yield the new source text
expect(await getSourceFromServer(uri, testPath), 'void f(int? i) {}');
});
}
test_lifecycle_preview_rerun_with_new_analysis_errors() async {
var origSourceText = 'void f(int i) {}';
var projectContents = simpleProject(sourceText: origSourceText);
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var testPath =
resourceProvider.pathContext.join(projectDir, 'lib', 'test.dart');
var newSourceText = 'void f(int? i) {}';
resourceProvider.getFile(testPath).writeAsStringSync(newSourceText);
// We haven't rerun, so getting the file details from the server should
// still yield the original source text, with informational space.
expect(await getSourceFromServer(uri, testPath), 'void f(int i) {}');
var response = await httpPost(uri.replace(path: 'rerun-migration'),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
var body = jsonDecode(response.body);
expect(body['success'], isFalse);
expect(body['errors'], hasLength(1));
var error = body['errors'].single;
expect(error['severity'], equals('error'));
expect(
error['message'],
equals(
"This requires the 'non-nullable' language feature to be enabled"));
expect(error['location'],
equals(resourceProvider.pathContext.join('lib', 'test.dart:1:11')));
expect(error['code'], equals('experiment_not_enabled'));
});
}
test_lifecycle_preview_serves_only_from_project_dir() async {
var crazyFunctionName = 'crazyFunctionNameThatHasNeverBeenSeenBefore';
var projectContents =
simpleProject(sourceText: 'void $crazyFunctionName() {}');
var mainProjectDir = createProjectDir(projectContents);
var otherProjectDir =
createProjectDir(projectContents, posixPath: '/other_project_dir');
var cli = _createCli();
await runWithPreviewServer(cli, [mainProjectDir], (url) async {
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
Future<http.Response> tryGetSourceFromProject(String projectDir) =>
tryGetSourceFromServer(
uri,
resourceProvider.pathContext
.join(projectDir, 'lib', 'test.dart'));
// To verify that we're forming the request correctly, make sure that we
// can read a file from mainProjectDir.
var response = await tryGetSourceFromProject(mainProjectDir);
assertHttpSuccess(response);
// And that crazyFunctionName appears in the response
expect(response.body, contains(crazyFunctionName));
// Now verify that making the exact same request from otherProjectDir
// fails.
response = await tryGetSourceFromProject(otherProjectDir);
expect(response.statusCode, 404);
// And check that we didn't leak any info through the 404 response.
expect(response.body, isNot(contains(crazyFunctionName)));
});
}
test_lifecycle_preview_stack_hint_action() async {
var projectContents = simpleProject(sourceText: 'int x;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var authToken = uri.queryParameters['authToken'];
var regionResponse = await httpGet(
uri.replace(
path: resourceProvider.pathContext
.toUri(resourceProvider.pathContext
.join(projectDir, 'lib', 'test.dart'))
.path,
queryParameters: {
'region': 'region',
'offset': '3',
'authToken': authToken
}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
var regionJson = jsonDecode(regionResponse.body);
var response = await httpPost(
uri.replace(
path: 'apply-hint', queryParameters: {'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(
regionJson['traces'][0]['entries'][0]['hintActions'][0]));
assertHttpSuccess(response);
assertProjectContents(
projectDir, simpleProject(sourceText: 'int/*?*/ x;'));
});
}
test_lifecycle_preview_stacktrace_link() async {
var projectContents = simpleProject(sourceText: 'int x;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
await runWithPreviewServer(cli, [projectDir], (url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
var uri = Uri.parse(url);
var authToken = uri.queryParameters['authToken'];
var regionUri = uri.replace(
path: resourceProvider.pathContext
.toUri(resourceProvider.pathContext
.join(projectDir, 'lib', 'test.dart'))
.path,
queryParameters: {
'region': 'region',
'offset': '3',
'authToken': authToken
});
var regionResponse = await httpGet(regionUri,
headers: {'Content-Type': 'application/json; charset=UTF-8'});
var regionJson = EditDetails.fromJson(jsonDecode(regionResponse.body));
final traceEntry = regionJson.traces![0].entries[0];
final uriPath = traceEntry.link!.href!;
// uriPath should be a working URI
final contentsResponse = await httpGet(
regionUri
.resolve(uriPath)
.replace(queryParameters: {'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(contentsResponse);
});
}
test_lifecycle_skip_import_check_disable() async {
var projectContents = simpleProject(
sourceText: '''
import 'package:foo/foo.dart';
import 'package:foo/bar.dart';
int f() => null;
''',
packageConfigText: _getPackageConfigText(
migrated: false, packagesMigrated: {'foo': false}));
var projectDir = createProjectDir(projectContents);
resourceProvider.newFile(packagePath('foo/lib/foo.dart'), '');
resourceProvider.newFile(packagePath('foo/lib/bar.dart'), '');
await assertRunFailure([projectDir], expectedExitCode: 1);
var output = logger.stdoutBuffer.toString();
expect(output, contains('Error: package has unmigrated dependencies'));
// Output should contain an indented, sorted list of all unmigrated
// dependencies.
expect(
output, contains('\n package:foo/bar.dart\n package:foo/foo.dart'));
// Output should mention that the user can rerun with `--skip-import-check`.
expect(output, contains('`--${CommandLineOptions.skipImportCheckFlag}`'));
}
test_lifecycle_skip_import_check_enable() async {
var projectContents = simpleProject(
sourceText: '''
import 'package:foo/foo.dart';
import 'package:foo/bar.dart';
int f() => null;
''',
packageConfigText: _getPackageConfigText(
migrated: false, packagesMigrated: {'foo': false}));
var projectDir = createProjectDir(projectContents);
resourceProvider.newFile(packagePath('foo/lib/foo.dart'), '');
resourceProvider.newFile(packagePath('foo/lib/bar.dart'), '');
var cli = _createCli();
await runWithPreviewServer(cli, ['--skip-import-check', projectDir],
(url) async {
await assertPreviewServerResponsive(url!);
var output = logger.stdoutBuffer.toString();
expect(output, contains('Warning: package has unmigrated dependencies'));
// Output should not mention the particular unmigrated dependencies.
expect(output, isNot(contains('package:foo')));
// Output should mention that the user can rerun without
// `--skip-import-check`.
expect(output, contains('`--${CommandLineOptions.skipImportCheckFlag}`'));
});
}
test_lifecycle_summary() async {
var projectContents = simpleProject();
var projectDir = createProjectDir(projectContents);
var summaryPath = resourceProvider.convertPath('/summary.json');
var cliRunner = _createCli().decodeCommandLineArgs(_parseArgs(
['--no-web-preview', '--summary', summaryPath, projectDir]))!;
await cliRunner.run();
var summaryData =
jsonDecode(resourceProvider.getFile(summaryPath).readAsStringSync());
expect(summaryData, TypeMatcher<Map>());
expect(summaryData, contains('changes'));
assertNormalExit(cliRunner);
}
test_lifecycle_summary_does_not_double_count_hint_removals() async {
var projectContents = simpleProject(sourceText: 'int/*?*/ x;');
var projectDir = createProjectDir(projectContents);
var summaryPath = resourceProvider.convertPath('/summary.json');
var cliRunner = _createCli().decodeCommandLineArgs(_parseArgs(
['--no-web-preview', '--summary', summaryPath, projectDir]))!;
await cliRunner.run();
assertNormalExit(cliRunner);
var summaryData =
jsonDecode(resourceProvider.getFile(summaryPath).readAsStringSync());
var separator = resourceProvider.pathContext.separator;
expect(summaryData['changes']['byPath']['lib${separator}test.dart'],
{'makeTypeNullableDueToHint': 1});
}
test_lifecycle_summary_rewritten_upon_rerun() async {
var projectContents = simpleProject(sourceText: 'int f(int/*?*/ i) => i;');
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
var summaryPath = resourceProvider.convertPath('/summary.json');
await runWithPreviewServer(cli, ['--summary', summaryPath, projectDir],
(url) async {
await assertPreviewServerResponsive(url!);
var summaryData =
jsonDecode(resourceProvider.getFile(summaryPath).readAsStringSync());
var separator = resourceProvider.pathContext.separator;
expect(summaryData['changes']['byPath']['lib${separator}test.dart'],
{'makeTypeNullableDueToHint': 1, 'makeTypeNullable': 1});
var testPath =
resourceProvider.pathContext.join(projectDir, 'lib', 'test.dart');
var newSourceText = 'int f(int/*?*/ i) => i + 1;';
resourceProvider.getFile(testPath).writeAsStringSync(newSourceText);
// Rerunning should create a new summary
var uri = Uri.parse(url);
var response = await httpPost(uri.replace(path: 'rerun-migration'),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
assertHttpSuccess(response);
summaryData =
jsonDecode(resourceProvider.getFile(summaryPath).readAsStringSync());
expect(summaryData['changes']['byPath']['lib${separator}test.dart'], {
'typeNotMadeNullable': 1,
'makeTypeNullableDueToHint': 1,
'checkExpression': 1
});
});
}
test_lifecycle_uri_error() async {
var projectContents = simpleProject(sourceText: '''
import 'package:does_not/exist.dart';
int f() => null;
''');
var projectDir = createProjectDir(projectContents);
await assertRunFailure([projectDir]);
var output = logger.stdoutBuffer.toString();
expect(output, contains('1 analysis issue found'));
expect(output, contains('uri_does_not_exist'));
expect(output, isNot(contains('erroneous migration suggestions')));
expect(output, contains('Run `dart pub get`'));
expect(output, contains('Try running `dart migrate` again'));
expect(output, isNot(contains('Set the lower SDK constraint')));
}
test_migrate_path_absolute() {
resourceProvider.newFolder(resourceProvider.pathContext
.join(resourceProvider.pathContext.current, 'foo'));
expect(
resourceProvider.pathContext
.isAbsolute(assertParseArgsSuccess(['foo']).directory),
isTrue);
}
test_migrate_path_file() {
resourceProvider.newFile(resourceProvider.pathContext.absolute('foo'), '');
expect(assertDecodeArgsFailure(['foo']), contains('foo is a file'));
}
test_migrate_path_non_existent() {
expect(assertDecodeArgsFailure(['foo']), contains('foo does not exist'));
}
test_migrate_path_none() {
expect(assertParseArgsSuccess([]).directory,
resourceProvider.pathContext.current);
}
test_migrate_path_normalized() {
expect(assertParseArgsSuccess(['foo/..']).directory, isNot(contains('..')));
}
test_migrate_path_one() {
resourceProvider.newFolder(resourceProvider.pathContext
.join(resourceProvider.pathContext.current, 'foo'));
expect(
assertParseArgsSuccess(['foo']).directory,
resourceProvider.pathContext
.join(resourceProvider.pathContext.current, 'foo'));
}
test_migrate_path_two() async {
var stderrText = await assertRunFailure(['foo', 'bar'], withUsage: true);
expect(stderrText, contains('No more than one path may be specified'));
}
test_option_preview_hostname() {
expect(
assertParseArgsSuccess(['--preview-hostname', 'any']).previewHostname,
'any');
}
test_option_preview_hostname_default() {
expect(assertParseArgsSuccess([]).previewHostname, 'localhost');
}
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() {
expect(assertDecodeArgsFailure(['--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 = _createCli();
var cliRunner = cli.decodeCommandLineArgs(_parseArgs([]))!;
expect(cliRunner.options.sdkPath,
cli._test.resourceProvider.convertPath(sdkRootPathPosix));
}
test_option_sdk_hidden() {
var optionName = '--sdk-path';
expect(_getHelpText(verbose: false), isNot(contains(optionName)));
expect(_getHelpText(verbose: true), contains(optionName));
}
test_option_summary() {
var summaryPath = resourceProvider.convertPath('/summary.json');
expect(assertParseArgsSuccess(['--summary', summaryPath]).summary,
summaryPath);
}
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_package_config_does_not_exist() async {
var projectContents = simpleProject()
..remove('.dart_tool/package_config.json');
var projectDir = createProjectDir(projectContents);
if (dartVersionIsNullSafeByDefault) {
// The lack of a package config file means the test file is already opted
// in.
await assertRunFailure(['--apply-changes', projectDir]);
_expectErrorIndicatingCodeIsAlreadyOptedIn();
} else {
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir,
simpleProject(migrated: true)
..remove('.dart_tool/package_config.json'));
}
}
test_package_config_is_missing_languageVersion() async {
var packageConfigText = '''
{
"configVersion": 2,
"packages": [
{
"name": "test",
"rootUri": "../",
"packageUri": "lib/"
}
]
}
''';
var projectContents = simpleProject(packageConfigText: packageConfigText);
var projectDir = createProjectDir(projectContents);
if (dartVersionIsNullSafeByDefault) {
// An omitted languageVersion field means the code is opted in to null
// safety.
await assertRunFailure(['--apply-changes', projectDir]);
_expectErrorIndicatingCodeIsAlreadyOptedIn();
} else {
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(projectDir,
simpleProject(migrated: true, packageConfigText: packageConfigText));
}
}
test_package_config_is_missing_this_package() async {
var packageConfigText = '''
{
"configVersion": 2,
"packages": [
]
}
''';
var projectContents = simpleProject(packageConfigText: packageConfigText);
var projectDir = createProjectDir(projectContents);
if (dartVersionIsNullSafeByDefault) {
// An omitted entry in the package config means the code is opted in to null
// safety.
await assertRunFailure(['--apply-changes', projectDir]);
_expectErrorIndicatingCodeIsAlreadyOptedIn();
} else {
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(projectDir,
simpleProject(migrated: true, packageConfigText: packageConfigText));
}
}
test_package_config_is_wrong_version() async {
var packageConfigText = '''
{
"configVersion": 3,
"packages": [
{
"name": "test",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "2.6"
}
]
}
''';
var projectContents = simpleProject(packageConfigText: packageConfigText);
var projectDir = createProjectDir(projectContents);
if (dartVersionIsNullSafeByDefault) {
// An unreadable package config means the code is opted in to null safety.
await assertRunFailure(['--apply-changes', projectDir]);
_expectErrorIndicatingCodeIsAlreadyOptedIn();
} else {
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(projectDir,
simpleProject(migrated: true, packageConfigText: packageConfigText));
}
}
test_pubspec_add_collection_dependency() async {
var projectContents = simpleProject(sourceText: '''
int firstEven(Iterable<int> x)
=> x.firstWhere((x) => x.isEven, orElse: () => null);
''', pubspecText: '''
name: test
environment:
sdk: '>=2.6.0 <3.0.0'
dependencies:
foo: ^1.2.3
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
expect(
logger.stdoutBuffer.toString(), contains('Please run `dart pub get`'));
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, sourceText: '''
import 'package:collection/collection.dart' show IterableExtension;
int? firstEven(Iterable<int> x)
=> x.firstWhereOrNull((x) => x.isEven);
''', pubspecText: '''
name: test
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
foo: ^1.2.3
collection: ^1.15.0-nullsafety.4
'''));
}
test_pubspec_add_dependency_and_environment_sections() async {
var projectContents = simpleProject(sourceText: '''
int firstEven(Iterable<int> x)
=> x.firstWhere((x) => x.isEven, orElse: () => null);
''', pubspecText: '''
name: test
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
expect(
logger.stdoutBuffer.toString(), contains('Please run `dart pub get`'));
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, sourceText: '''
import 'package:collection/collection.dart' show IterableExtension;
int? firstEven(Iterable<int> x)
=> x.firstWhereOrNull((x) => x.isEven);
''',
// Note: section order is weird, but it's valid and this is a rare use
// case.
pubspecText: '''
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
collection: ^1.15.0-nullsafety.4
name: test
'''));
}
test_pubspec_add_dependency_section() async {
var projectContents = simpleProject(sourceText: '''
int firstEven(Iterable<int> x)
=> x.firstWhere((x) => x.isEven, orElse: () => null);
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
expect(
logger.stdoutBuffer.toString(), contains('Please run `dart pub get`'));
// The Dart source code should still be migrated.
assertProjectContents(projectDir, simpleProject(migrated: true, sourceText: '''
import 'package:collection/collection.dart' show IterableExtension;
int? firstEven(Iterable<int> x)
=> x.firstWhereOrNull((x) => x.isEven);
''',
// Note: `dependencies` section is in a weird place, but it's valid and
// this is a rare use case.
pubspecText: '''
dependencies:
collection: ^1.15.0-nullsafety.4
name: test
environment:
sdk: '>=2.12.0 <3.0.0'
'''));
}
test_pubspec_does_not_exist() async {
var projectContents = simpleProject()..remove('pubspec.yaml');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir,
simpleProject(
migrated: true,
// The package config file should not have been touched.
packageConfigText: _getPackageConfigText(migrated: false))
..remove('pubspec.yaml'));
}
test_pubspec_environment_is_missing_sdk() async {
var projectContents = simpleProject(pubspecText: '''
name: test
environment:
foo: 1
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, pubspecText: '''
name: test
environment:
foo: 1
sdk: '>=2.12.0 <3.0.0'
'''));
}
test_pubspec_environment_is_not_a_map() async {
var pubspecText = '''
name: test
environment: 1
''';
var projectContents = simpleProject(pubspecText: pubspecText);
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, pubspecText: pubspecText));
}
test_pubspec_environment_sdk_is_exact_version() async {
var pubspecText = '''
name: test
environment:
sdk: '2.0.0'
''';
var projectContents = simpleProject(pubspecText: pubspecText);
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir,
simpleProject(
migrated: true,
pubspecText: pubspecText,
// The package config file should not have been touched.
packageConfigText: _getPackageConfigText(migrated: false)));
}
test_pubspec_environment_sdk_is_missing_min() async {
var pubspecText = '''
name: test
environment:
sdk: '<3.0.0'
''';
var projectContents = simpleProject(pubspecText: pubspecText);
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir,
simpleProject(
migrated: true,
pubspecText: pubspecText,
// The package config file should not have been touched.
packageConfigText: _getPackageConfigText(migrated: false)));
}
test_pubspec_environment_sdk_is_not_string() async {
var pubspecText = '''
name: test
environment:
sdk: 1
''';
var projectContents = simpleProject(pubspecText: pubspecText);
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir,
simpleProject(
migrated: true,
pubspecText: pubspecText,
// The package config file should not have been touched.
packageConfigText: _getPackageConfigText(migrated: false)));
}
test_pubspec_environment_sdk_is_union() async {
var pubspecText = '''
name: test
environment:
sdk: '>=2.0.0 <2.1.0 >=2.2.0 <3.0.0'
''';
var projectContents = simpleProject(pubspecText: pubspecText);
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir,
simpleProject(
migrated: true,
pubspecText: pubspecText,
// The package config file should not have been touched.
packageConfigText: _getPackageConfigText(migrated: false)));
}
test_pubspec_has_unusual_max_sdk_constraint() async {
// No one should be using a weird max SDK constraint like this. If they are
// doing so, we'll fix it to 3.0.0.
var projectContents = simpleProject(pubspecText: '''
name: test
environment:
sdk: '>=2.6.0 <2.17.4'
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, pubspecText: '''
name: test
environment:
sdk: '>=2.12.0 <3.0.0'
'''));
}
test_pubspec_is_missing_environment() async {
var projectContents = simpleProject(pubspecText: '''
name: test
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
// The Dart source code should still be migrated.
assertProjectContents(projectDir, simpleProject(migrated: true, pubspecText:
// This is strange-looking, but valid.
'''
environment:
sdk: '>=2.12.0 <3.0.0'
name: test
'''));
}
test_pubspec_is_not_a_map() async {
var projectContents = simpleProject(pubspecText: 'not-a-map');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
var message = await assertErrorExit(
cliRunner, () async => await cliRunner.run(),
withUsage: false);
expect(message, contains('Failed to parse pubspec file'));
}
test_pubspec_preserve_collection_dependency() async {
var projectContents = simpleProject(sourceText: '''
int firstEven(Iterable<int> x)
=> x.firstWhere((x) => x.isEven, orElse: () => null);
''', pubspecText: '''
name: test
environment:
sdk: '>=2.6.0 <3.0.0'
dependencies:
collection: ^1.16.0
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
expect(logger.stdoutBuffer.toString(),
isNot(contains('Please run `dart pub get`')));
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, sourceText: '''
import 'package:collection/collection.dart' show IterableExtension;
int? firstEven(Iterable<int> x)
=> x.firstWhereOrNull((x) => x.isEven);
''', pubspecText: '''
name: test
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
collection: ^1.16.0
'''));
}
test_pubspec_update_collection_dependency() async {
var projectContents = simpleProject(sourceText: '''
int firstEven(Iterable<int> x)
=> x.firstWhere((x) => x.isEven, orElse: () => null);
''', pubspecText: '''
name: test
environment:
sdk: '>=2.6.0 <3.0.0'
dependencies:
collection: ^1.14.0
''');
var projectDir = createProjectDir(projectContents);
var cliRunner = _createCli()
.decodeCommandLineArgs(_parseArgs(['--apply-changes', projectDir]))!;
await cliRunner.run();
expect(
logger.stdoutBuffer.toString(), contains('Please run `dart pub get`'));
// The Dart source code should still be migrated.
assertProjectContents(
projectDir, simpleProject(migrated: true, sourceText: '''
import 'package:collection/collection.dart' show IterableExtension;
int? firstEven(Iterable<int> x)
=> x.firstWhereOrNull((x) => x.isEven);
''', pubspecText: '''
name: test
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
collection: ^1.15.0-nullsafety.4
'''));
}
test_uses_physical_resource_provider_by_default() {
var cli = MigrationCli(binaryName: 'nnbd_migration');
expect(cli.resourceProvider, same(PhysicalResourceProvider.INSTANCE));
}
Future<http.Response> tryGetSourceFromServer(Uri uri, String path) async {
var authToken = uri.queryParameters['authToken'];
return await httpGet(
uri.replace(
path: resourceProvider.pathContext.toUri(path).path,
queryParameters: {'inline': 'true', 'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'});
}
_MigrationCli _createCli() {
mock_sdk.createMockSdk(
resourceProvider: resourceProvider,
root: resourceProvider.newFolder(
resourceProvider.convertPath(sdkRootPathPosix),
),
);
return _MigrationCli(this);
}
void _expectErrorIndicatingCodeIsAlreadyOptedIn() {
var errorOutput = logger.stdoutBuffer.toString();
expect(errorOutput, contains('1 analysis issue found:'));
expect(
errorOutput,
contains("A value of type 'Null' can't be returned from the function "
"'f' because it has a return type of 'int'"));
expect(errorOutput, contains('Set the lower SDK constraint'));
}
String _getHelpText({required bool verbose}) {
var cliRunner = _createCli().decodeCommandLineArgs(_parseArgs(
['--${CommandLineOptions.helpFlag}', if (verbose) '--verbose']));
expect(cliRunner, isNull);
var helpText = logger.stderrBuffer.toString();
return helpText;
}
String _getPackageConfigText(
{required bool migrated, Map<String, bool> packagesMigrated = const {}}) {
Object makePackageEntry(String name, bool migrated, {String? rootUri}) {
rootUri ??=
resourceProvider.pathContext.toUri(packagePath(name)).toString();
return {
'name': name,
'rootUri': rootUri,
'packageUri': 'lib/',
'languageVersion': migrated ? '2.12' : '2.6'
};
}
var json = {
'configVersion': 2,
'packages': [
makePackageEntry('test', migrated, rootUri: '../'),
for (var entry in packagesMigrated.entries)
makePackageEntry(entry.key, entry.value)
]
};
return '${JsonEncoder.withIndent(' ').convert(json)}\n';
}
ArgResults _parseArgs(List<String> args) {
return MigrationCli.createParser().parse(args);
}
Future<void> _tellPreviewToApplyChanges(String url) async {
var uri = Uri.parse(url);
var authToken = uri.queryParameters['authToken'];
var response = await httpPost(
uri.replace(
path: PreviewSite.applyMigrationPath,
queryParameters: {'authToken': authToken}),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: json.encode({'navigationTree': []}));
assertHttpSuccess(response);
}
}
@reflectiveTest
class _MigrationCliTestPosix extends _MigrationCliTestBase
with _MigrationCliTestMethods {
@override
final MemoryResourceProvider resourceProvider;
_MigrationCliTestPosix()
: resourceProvider = MemoryResourceProvider(
context: path.style == path.Style.posix
? null
: path.Context(
style: path.Style.posix, current: '/working_dir'));
}
@reflectiveTest
class _MigrationCliTestWindows extends _MigrationCliTestBase
with _MigrationCliTestMethods {
@override
final MemoryResourceProvider resourceProvider;
_MigrationCliTestWindows()
: resourceProvider = MemoryResourceProvider(
context: path.style == path.Style.windows
? null
: path.Context(
style: path.Style.windows, current: 'C:\\working_dir'));
}