dart-sdk/pkg/nnbd_migration/test/migration_cli_test.dart
Paul Berry 22a405bf8a Add a test case for #44394
This issue (Migration tool null safety detection fails when tests use
path imports) was reported in December of 2020 and I can't reproduce
it.  I believe there have been changes in analyzer package resolution
since then, aimed precisely at helping users who use this sort of
improper path import.  So it is likely that those changes fixed this
bug.

I'm adding a test case to ensure that the bug isn't accidentally
un-fixed by future changes.

Bug: https://github.com/dart-lang/sdk/issues/44394
Change-Id: I81490e545aa41196c3c69bc4d74cd481079d59ba
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/259200
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
2022-09-14 16:10:42 +00:00

2286 lines
85 KiB
Dart

// 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({super.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 super.cli, super.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_handle_improper_lib_import() async {
Map<String, String?> computeProjectContents({required bool migrated}) => {
'pubspec.yaml': '''
name: test
environment:
sdk: '${migrated ? '>=2.12.0 <3.0.0' : '>=2.6.0 <3.0.0'}'
''',
'.dart_tool/package_config.json':
_getPackageConfigText(migrated: migrated),
'lib/foo.dart': '''
int${migrated ? '?' : ''} f() => null;
''',
'test/foo_test.dart': '''
import '../lib/foo.dart';
int${migrated ? '?' : ''} g() => f();
''',
};
var projectContents = computeProjectContents(migrated: false);
var projectDir = createProjectDir(projectContents);
var cli = _createCli();
bool applyHookCalled = false;
cli._onApplyHook = () {
expect(applyHookCalled, false);
applyHookCalled = true;
// Changes should have been made
assertProjectContents(projectDir, computeProjectContents(migrated: true));
};
await runWithPreviewServer(cli, ['--skip-import-check', projectDir],
(url) async {
expect(
logger.stdoutBuffer.toString(), contains('No analysis issues found'));
await assertPreviewServerResponsive(url!);
await _tellPreviewToApplyChanges(url);
expect(applyHookCalled, true);
var output = logger.stdoutBuffer.toString();
expect(output,
isNot(contains('Warning: package has unmigrated dependencies')));
// Output should not mention that the user can rerun without
// `--skip-import-check`.
expect(output,
isNot(contains('`--${CommandLineOptions.skipImportCheckFlag}`')));
});
}
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'));
}