blob: c8da7d3174b854dddf897d93b391b35287e860f0 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
import 'package:flutter_migrate/src/base/context.dart';
import 'package:flutter_migrate/src/base/file_system.dart';
import 'package:flutter_migrate/src/base/io.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; // flutter_ignore: package_path_import
import 'package:test_api/test_api.dart' // ignore: deprecated_member_use
as test_package show test;
import 'package:test_api/test_api.dart' // ignore: deprecated_member_use
hide
test;
import 'test_utils.dart';
export 'package:test_api/test_api.dart' // ignore: deprecated_member_use
hide
isInstanceOf,
test;
bool tryToDelete(FileSystemEntity fileEntity) {
// This should not be necessary, but it turns out that
// on Windows it's common for deletions to fail due to
// bogus (we think) "access denied" errors.
try {
if (fileEntity.existsSync()) {
fileEntity.deleteSync(recursive: true);
return true;
}
} on FileSystemException catch (error) {
// We print this so that it's visible in the logs, to get an idea of how
// common this problem is, and if any patterns are ever noticed by anyone.
// ignore: avoid_print
print('Failed to delete ${fileEntity.path}: $error');
}
return false;
}
/// Gets the path to the root of the Flutter repository.
///
/// This will first look for a `FLUTTER_ROOT` environment variable. If the
/// environment variable is set, it will be returned. Otherwise, this will
/// deduce the path from `platform.script`.
String getFlutterRoot() {
if (io.Platform.environment.containsKey('FLUTTER_ROOT')) {
return io.Platform.environment['FLUTTER_ROOT']!;
}
Error invalidScript() => StateError(
'Could not determine flutter_tools/ path from script URL (${io.Platform.script}); consider setting FLUTTER_ROOT explicitly.');
Uri scriptUri;
switch (io.Platform.script.scheme) {
case 'file':
scriptUri = io.Platform.script;
break;
case 'data':
final RegExp flutterTools = RegExp(
r'(file://[^"]*[/\\]flutter_tools[/\\][^"]+\.dart)',
multiLine: true);
final Match? match =
flutterTools.firstMatch(Uri.decodeFull(io.Platform.script.path));
if (match == null) {
throw invalidScript();
}
scriptUri = Uri.parse(match.group(1)!);
break;
default:
throw invalidScript();
}
final List<String> parts = path.split(fileSystem.path.fromUri(scriptUri));
final int toolsIndex = parts.indexOf('flutter_tools');
if (toolsIndex == -1) {
throw invalidScript();
}
final String toolsPath = path.joinAll(parts.sublist(0, toolsIndex + 1));
return path.normalize(path.join(toolsPath, '..', '..'));
}
String getMigratePackageRoot() {
return io.Directory.current.path;
}
String getMigrateMain() {
return fileSystem.path
.join(getMigratePackageRoot(), 'bin', 'flutter_migrate.dart');
}
Future<ProcessResult> runMigrateCommand(List<String> args,
{String? workingDirectory}) {
final List<String> commandArgs = <String>['dart', 'run', getMigrateMain()];
commandArgs.addAll(args);
return processManager.run(commandArgs, workingDirectory: workingDirectory);
}
/// The tool overrides `test` to ensure that files created under the
/// system temporary directory are deleted after each test by calling
/// `LocalFileSystem.dispose()`.
@isTest
void test(
String description,
FutureOr<void> Function() body, {
String? testOn,
dynamic skip,
List<String>? tags,
Map<String, dynamic>? onPlatform,
int? retry,
Timeout? timeout,
}) {
test_package.test(
description,
() async {
addTearDown(() async {
await fileSystem.dispose();
});
return body();
},
skip: skip,
tags: tags,
onPlatform: onPlatform,
retry: retry,
testOn: testOn,
timeout: timeout,
// We don't support "timeout"; see ../../dart_test.yaml which
// configures all tests to have a 15 minute timeout which should
// definitely be enough.
);
}
/// Executes a test body in zone that does not allow context-based injection.
///
/// For classes which have been refactored to exclude context-based injection
/// or globals like [fs] or [platform], prefer using this test method as it
/// will prevent accidentally including these context getters in future code
/// changes.
///
/// For more information, see https://github.com/flutter/flutter/issues/47161
@isTest
void testWithoutContext(
String description,
FutureOr<void> Function() body, {
String? testOn,
dynamic skip,
List<String>? tags,
Map<String, dynamic>? onPlatform,
int? retry,
Timeout? timeout,
}) {
return test(
description,
() async {
return runZoned(body, zoneValues: <Object, Object>{
contextKey: const _NoContext(),
});
},
skip: skip,
tags: tags,
onPlatform: onPlatform,
retry: retry,
testOn: testOn,
timeout: timeout,
// We support timeout here due to the packages repo not setting default
// timeout to 15min.
);
}
/// An implementation of [AppContext] that throws if context.get is called in the test.
///
/// The intention of the class is to ensure we do not accidentally regress when
/// moving towards more explicit dependency injection by accidentally using
/// a Zone value in place of a constructor parameter.
class _NoContext implements AppContext {
const _NoContext();
@override
T get<T>() {
throw UnsupportedError('context.get<$T> is not supported in test methods. '
'Use Testbed or testUsingContext if accessing Zone injected '
'values.');
}
@override
String get name => 'No Context';
@override
Future<V> run<V>({
required FutureOr<V> Function() body,
String? name,
Map<Type, Generator>? overrides,
Map<Type, Generator>? fallbacks,
ZoneSpecification? zoneSpecification,
}) async {
return body();
}
}
/// Matcher for functions that throw [AssertionError].
final Matcher throwsAssertionError = throwsA(isA<AssertionError>());