common.dart 7.12 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
25
Matcher isInstanceOf<T>() => test_package.TypeMatcher<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

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

/// Matcher for [ToolExit]s.
107
final Matcher isToolExit = isInstanceOf<ToolExit>();
108 109

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

/// Matcher for [ProcessExit]s.
117
final Matcher isProcessExit = isInstanceOf<ProcessExit>();
118

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

133 134 135 136 137 138 139 140 141 142
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);
  } catch(e, trace) {
    fail('ToolExit expected, got $e\n$trace');
  }
}
143 144 145 146 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

/// 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();
  }
}