common.dart 7.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:args/command_runner.dart';
8
import 'package:flutter_tools/src/base/common.dart';
9
import 'package:flutter_tools/src/base/context.dart';
10
import 'package:flutter_tools/src/base/file_system.dart';
11

12
import 'package:flutter_tools/src/base/process.dart';
13
import 'package:flutter_tools/src/commands/create.dart';
14 15
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
16
import 'package:flutter_tools/src/globals.dart' as globals;
17 18
import 'package:meta/meta.dart';
import 'package:test_api/test_api.dart' as test_package show TypeMatcher, test; // ignore: deprecated_member_use
19 20
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf; // ignore: deprecated_member_use
// ignore: deprecated_member_use
21
export 'package:test_core/test_core.dart' hide TypeMatcher, isInstanceOf; // Defines a 'package:test' shim.
22 23 24

/// A matcher that compares the type of the actual value to the type argument T.
// TODO(ianh): Remove this once https://github.com/dart-lang/matcher/issues/98 is fixed
Dan Field's avatar
Dan Field committed
25
test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>();
26

27 28 29 30 31 32 33 34 35 36 37
void tryToDelete(Directory directory) {
  // 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 {
    directory.deleteSync(recursive: true);
  } on FileSystemException catch (error) {
    print('Failed to delete ${directory.path}: $error');
  }
}

38 39 40 41 42 43
/// 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() {
44 45
  if (globals.platform.environment.containsKey('FLUTTER_ROOT')) {
    return globals.platform.environment['FLUTTER_ROOT'];
46
  }
47

48
  Error invalidScript() => StateError('Could not determine flutter_tools/ path from script URL (${globals.platform.script}); consider setting FLUTTER_ROOT explicitly.');
49 50

  Uri scriptUri;
51
  switch (globals.platform.script.scheme) {
52
    case 'file':
53
      scriptUri = globals.platform.script;
54 55
      break;
    case 'data':
56
      final RegExp flutterTools = RegExp(r'(file://[^"]*[/\\]flutter_tools[/\\][^"]+\.dart)', multiLine: true);
57
      final Match match = flutterTools.firstMatch(Uri.decodeFull(globals.platform.script.path));
58
      if (match == null) {
59
        throw invalidScript();
60
      }
61 62 63 64 65 66
      scriptUri = Uri.parse(match.group(1));
      break;
    default:
      throw invalidScript();
  }

67
  final List<String> parts = globals.fs.path.split(globals.fs.path.fromUri(scriptUri));
68
  final int toolsIndex = parts.indexOf('flutter_tools');
69
  if (toolsIndex == -1) {
70
    throw invalidScript();
71
  }
72 73
  final String toolsPath = globals.fs.path.joinAll(parts.sublist(0, toolsIndex + 1));
  return globals.fs.path.normalize(globals.fs.path.join(toolsPath, '..', '..'));
74 75
}

76
CommandRunner<void> createTestCommandRunner([ FlutterCommand command ]) {
77
  final FlutterCommandRunner runner = FlutterCommandRunner();
78
  if (command != null) {
79
    runner.addCommand(command);
80
  }
81
  return runner;
82
}
83 84

/// Updates [path] to have a modification time [seconds] from now.
85 86 87 88 89
void updateFileModificationTime(
  String path,
  DateTime baseTime,
  int seconds,
) {
90
  final DateTime modificationTime = baseTime.add(Duration(seconds: seconds));
91
  globals.fs.file(path).setLastModifiedSync(modificationTime);
92
}
93

Dan Field's avatar
Dan Field committed
94 95 96
/// Matcher for functions that throw [AssertionError].
final Matcher throwsAssertionError = throwsA(isA<AssertionError>());

97
/// Matcher for functions that throw [ToolExit].
98
Matcher throwsToolExit({ int exitCode, Pattern message }) {
99
  Matcher matcher = isToolExit;
100
  if (exitCode != null) {
101
    matcher = allOf(matcher, (ToolExit e) => e.exitCode == exitCode);
102 103
  }
  if (message != null) {
104
    matcher = allOf(matcher, (ToolExit e) => e.message?.contains(message) ?? false);
105
  }
106
  return throwsA(matcher);
107 108 109
}

/// Matcher for [ToolExit]s.
Dan Field's avatar
Dan Field committed
110
final test_package.TypeMatcher<ToolExit> isToolExit = isA<ToolExit>();
111 112

/// Matcher for functions that throw [ProcessExit].
113
Matcher throwsProcessExit([ dynamic exitCode ]) {
114 115 116 117 118 119
  return exitCode == null
      ? throwsA(isProcessExit)
      : throwsA(allOf(isProcessExit, (ProcessExit e) => e.exitCode == exitCode));
}

/// Matcher for [ProcessExit]s.
Dan Field's avatar
Dan Field committed
120
final test_package.TypeMatcher<ProcessExit> isProcessExit = isA<ProcessExit>();
121

122 123
/// Creates a flutter project in the [temp] directory using the
/// [arguments] list if specified, or `--no-pub` if not.
124
/// Returns the path to the flutter project.
125
Future<String> createProject(Directory temp, { List<String> arguments }) async {
126
  arguments ??= <String>['--no-pub'];
127
  final String projectPath = globals.fs.path.join(temp.path, 'flutter_project');
128
  final CreateCommand command = CreateCommand();
129
  final CommandRunner<void> runner = createTestCommandRunner(command);
130
  await runner.run(<String>['create', ...arguments, projectPath]);
131
  // Created `.packages` since it's not created when the flag `--no-pub` is passed.
132
  globals.fs.file(globals.fs.path.join(projectPath, '.packages')).createSync();
133
  return projectPath;
134 135
}

136 137 138 139 140 141
Future<void> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
  try {
    await future;
    fail('ToolExit expected, but nothing thrown');
  } on ToolExit catch(e) {
    expect(e.message, messageMatcher);
142 143
  // Catch all exceptions to give a better test failure message.
  } catch (e, trace) { // ignore: avoid_catches_without_on_clauses
144 145 146
    fail('ToolExit expected, got $e\n$trace');
  }
}
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210

/// Executes a test body in zone that does not allow context-based injection.
///
/// For classes which have been refactored to excluded 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> body(), {
  String testOn,
  Timeout timeout,
  bool skip,
  List<String> tags,
  Map<String, dynamic> onPlatform,
  int retry,
  }) {
  return test_package.test(
    description, () async {
      return runZoned(body, zoneValues: <Object, Object>{
        contextKey: const NoContext(),
      });
    },
    timeout: timeout,
    skip: skip,
    tags: tags,
    onPlatform: onPlatform,
    retry: retry,
    testOn: testOn,
  );
}

/// 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>({
    FutureOr<V> Function() body,
    String name,
    Map<Type, Generator> overrides,
    Map<Type, Generator> fallbacks,
    ZoneSpecification zoneSpecification,
  }) async {
    return body();
  }
}