mocks.dart 19.7 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
import 'dart:async';
6
import 'dart:convert';
7
import 'dart:io' as io show IOSink, ProcessSignal, Stdout, StdoutException;
8

9
import 'package:package_config/package_config.dart';
10 11
import 'package:platform/platform.dart';

12
import 'package:flutter_tools/src/android/android_device.dart';
13
import 'package:flutter_tools/src/android/android_sdk.dart' show AndroidSdk;
14
import 'package:flutter_tools/src/application_package.dart';
Dan Field's avatar
Dan Field committed
15
import 'package:flutter_tools/src/base/bot_detector.dart';
16
import 'package:flutter_tools/src/base/context.dart';
17
import 'package:flutter_tools/src/base/file_system.dart' hide IOSink;
18
import 'package:flutter_tools/src/base/io.dart';
19
import 'package:flutter_tools/src/build_info.dart';
20
import 'package:flutter_tools/src/compile.dart';
21
import 'package:flutter_tools/src/devfs.dart';
22
import 'package:flutter_tools/src/device.dart';
23 24
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
25
import 'package:flutter_tools/src/project.dart';
26
import 'package:flutter_tools/src/runner/flutter_command.dart';
27
import 'package:flutter_tools/src/globals.dart' as globals;
28
import 'package:mockito/mockito.dart';
29
import 'package:process/process.dart';
30 31

import 'common.dart';
32

33
// TODO(fujino): replace FakePlatform.fromPlatform() with FakePlatform()
34 35 36 37 38
final Generator kNoColorTerminalPlatform = () {
  return FakePlatform.fromPlatform(
    const LocalPlatform()
  )..stdoutSupportsAnsi = false;
};
39

40 41
class MockApplicationPackageStore extends ApplicationPackageStore {
  MockApplicationPackageStore() : super(
42
    android: AndroidApk(
Adam Barth's avatar
Adam Barth committed
43
      id: 'io.flutter.android.mock',
44
      file: globals.fs.file('/mock/path/to/android/SkyShell.apk'),
45
      versionCode: 1,
46
      launchActivity: 'io.flutter.android.mock.MockActivity',
Adam Barth's avatar
Adam Barth committed
47
    ),
48
    iOS: BuildableIOSApp(MockIosProject(), MockIosProject.bundleId, MockIosProject.appBundleName),
49
  );
50 51
}

52 53 54 55 56 57 58 59 60 61 62 63
class MockApplicationPackageFactory extends Mock implements ApplicationPackageFactory {
  final MockApplicationPackageStore _store = MockApplicationPackageStore();

  @override
  Future<ApplicationPackage> getPackageForPlatform(
    TargetPlatform platform, {
    File applicationBinary,
  }) async {
    return _store.getPackageForPlatform(platform);
  }
}

64 65 66
/// An SDK installation with several SDK levels (19, 22, 23).
class MockAndroidSdk extends Mock implements AndroidSdk {
  static Directory createSdkDirectory({
67 68
    bool withAndroidN = false,
    bool withSdkManager = true,
69 70
    bool withPlatformTools = true,
    bool withBuildTools = true,
71
  }) {
72 73 74
    final Directory dir = globals.fs.systemTempDirectory.createTempSync('flutter_mock_android_sdk.');
    final String exe = globals.platform.isWindows ? '.exe' : '';
    final String bat = globals.platform.isWindows ? '.bat' : '';
75

76
    _createDir(dir, 'licenses');
77

78 79 80 81 82 83 84 85
    if (withPlatformTools) {
      _createSdkFile(dir, 'platform-tools/adb$exe');
    }

    if (withBuildTools) {
      _createSdkFile(dir, 'build-tools/19.1.0/aapt$exe');
      _createSdkFile(dir, 'build-tools/22.0.1/aapt$exe');
      _createSdkFile(dir, 'build-tools/23.0.2/aapt$exe');
86
      if (withAndroidN) {
87
        _createSdkFile(dir, 'build-tools/24.0.0-preview/aapt$exe');
88
      }
89
    }
90 91 92 93 94 95 96 97

    _createSdkFile(dir, 'platforms/android-22/android.jar');
    _createSdkFile(dir, 'platforms/android-23/android.jar');
    if (withAndroidN) {
      _createSdkFile(dir, 'platforms/android-N/android.jar');
      _createSdkFile(dir, 'platforms/android-N/build.prop', contents: _buildProp);
    }

98
    if (withSdkManager) {
99
      _createSdkFile(dir, 'tools/bin/sdkmanager$bat');
100
    }
101 102 103 104 105 106 107 108 109 110 111 112 113

    return dir;
  }

  static void _createSdkFile(Directory dir, String filePath, { String contents }) {
    final File file = dir.childFile(filePath);
    file.createSync(recursive: true);
    if (contents != null) {
      file.writeAsStringSync(contents, flush: true);
    }
  }

  static void _createDir(Directory dir, String path) {
114
    final Directory directory = globals.fs.directory(globals.fs.path.join(dir.path, path));
115 116 117 118 119 120 121 122 123 124
    directory.createSync(recursive: true);
  }

  static const String _buildProp = r'''
ro.build.version.incremental=1624448
ro.build.version.sdk=24
ro.build.version.codename=REL
''';
}

125
/// A strategy for creating Process objects from a list of commands.
126
typedef ProcessFactory = Process Function(List<String> command);
127 128

/// A ProcessManager that starts Processes by delegating to a ProcessFactory.
129
class MockProcessManager extends Mock implements ProcessManager {
130
  ProcessFactory processFactory = (List<String> commands) => MockProcess();
131 132
  bool canRunSucceeds = true;
  bool runSucceeds = true;
133 134
  List<String> commands;

135
  @override
136
  bool canRun(dynamic command, { String workingDirectory }) => canRunSucceeds;
137

138 139 140 141 142
  @override
  Future<Process> start(
    List<dynamic> command, {
    String workingDirectory,
    Map<String, String> environment,
143 144
    bool includeParentEnvironment = true,
    bool runInShell = false,
145
    ProcessStartMode mode = ProcessStartMode.normal,
146
  }) {
147
    final List<String> commands = command.cast<String>();
148
    if (!runSucceeds) {
149 150
      final String executable = commands[0];
      final List<String> arguments = commands.length > 1 ? commands.sublist(1) : <String>[];
151
      throw ProcessException(executable, arguments);
152 153
    }

154 155
    this.commands = commands;
    return Future<Process>.value(processFactory(commands));
156 157 158
  }
}

159 160 161
/// A function that generates a process factory that gives processes that fail
/// a given number of times before succeeding. The returned processes will
/// fail after a delay if one is supplied.
162
ProcessFactory flakyProcessFactory({
163 164 165 166 167 168
  int flakes,
  bool Function(List<String> command) filter,
  Duration delay,
  Stream<List<int>> Function() stdout,
  Stream<List<int>> Function() stderr,
}) {
169
  int flakesLeft = flakes;
170 171
  stdout ??= () => const Stream<List<int>>.empty();
  stderr ??= () => const Stream<List<int>>.empty();
172
  return (List<String> command) {
173 174 175
    if (filter != null && !filter(command)) {
      return MockProcess();
    }
176
    if (flakesLeft == 0) {
177 178 179 180 181
      return MockProcess(
        exitCode: Future<int>.value(0),
        stdout: stdout(),
        stderr: stderr(),
      );
182 183 184 185 186 187 188 189
    }
    flakesLeft = flakesLeft - 1;
    Future<int> exitFuture;
    if (delay == null) {
      exitFuture = Future<int>.value(-9);
    } else {
      exitFuture = Future<int>.delayed(delay, () => Future<int>.value(-9));
    }
190 191 192 193 194
    return MockProcess(
      exitCode: exitFuture,
      stdout: stdout(),
      stderr: stderr(),
    );
195 196 197
  };
}

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
/// Creates a mock process that returns with the given [exitCode], [stdout] and [stderr].
Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) {
  final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
    utf8.encode(stdout),
  ]);
  final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
    utf8.encode(stderr),
  ]);
  final Process process = MockBasicProcess();

  when(process.stdout).thenAnswer((_) => stdoutStream);
  when(process.stderr).thenAnswer((_) => stderrStream);
  when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
  return process;
}

class MockBasicProcess extends Mock implements Process {}

216 217 218
/// A process that exits successfully with no output and ignores all input.
class MockProcess extends Mock implements Process {
  MockProcess({
219
    this.pid = 1,
220 221
    Future<int> exitCode,
    Stream<List<int>> stdin,
222 223
    this.stdout = const Stream<List<int>>.empty(),
    this.stderr = const Stream<List<int>>.empty(),
224
  }) : exitCode = exitCode ?? Future<int>.value(0),
225
       stdin = stdin as IOSink ?? MemoryIOSink();
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242

  @override
  final int pid;

  @override
  final Future<int> exitCode;

  @override
  final io.IOSink stdin;

  @override
  final Stream<List<int>> stdout;

  @override
  final Stream<List<int>> stderr;
}

243
/// A fake process implementation which can be provided all necessary values.
244 245 246 247 248 249 250 251
class FakeProcess implements Process {
  FakeProcess({
    this.pid = 1,
    Future<int> exitCode,
    Stream<List<int>> stdin,
    this.stdout = const Stream<List<int>>.empty(),
    this.stderr = const Stream<List<int>>.empty(),
  }) : exitCode = exitCode ?? Future<int>.value(0),
252
       stdin = stdin as IOSink ?? MemoryIOSink();
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274

  @override
  final int pid;

  @override
  final Future<int> exitCode;

  @override
  final io.IOSink stdin;

  @override
  final Stream<List<int>> stdout;

  @override
  final Stream<List<int>> stderr;

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
    return true;
  }
}

275 276 277
/// A process that prompts the user to proceed, then asynchronously writes
/// some lines to stdout before it exits.
class PromptingProcess implements Process {
278 279 280 281
  PromptingProcess({
    bool stdinError = false,
  }) : _stdin = CompleterIOSink(throwOnAdd: stdinError);

282
  Future<void> showPrompt(String prompt, List<String> outputLines) async {
283 284 285 286 287 288 289 290 291
    try {
      _stdoutController.add(utf8.encode(prompt));
      final List<int> bytesOnStdin = await _stdin.future;
      // Echo stdin to stdout.
      _stdoutController.add(bytesOnStdin);
      if (bytesOnStdin.isNotEmpty && bytesOnStdin[0] == utf8.encode('y')[0]) {
        for (final String line in outputLines) {
          _stdoutController.add(utf8.encode('$line\n'));
        }
292
      }
293 294
    } finally {
      await _stdoutController.close();
295 296 297
    }
  }

298
  final StreamController<List<int>> _stdoutController = StreamController<List<int>>();
299
  final CompleterIOSink _stdin;
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321

  @override
  Stream<List<int>> get stdout => _stdoutController.stream;

  @override
  Stream<List<int>> get stderr => const Stream<List<int>>.empty();

  @override
  IOSink get stdin => _stdin;

  @override
  Future<int> get exitCode async {
    await _stdoutController.done;
    return 0;
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => null;
}

/// An IOSink that completes a future with the first line written to it.
class CompleterIOSink extends MemoryIOSink {
322 323 324 325 326 327
  CompleterIOSink({
    this.throwOnAdd = false,
  });

  final bool throwOnAdd;

328
  final Completer<List<int>> _completer = Completer<List<int>>();
329 330 331 332 333

  Future<List<int>> get future => _completer.future;

  @override
  void add(List<int> data) {
334
    if (!_completer.isCompleted) {
335 336 337 338 339
      // When throwOnAdd is true, complete with empty so any expected output
      // doesn't appear.
      _completer.complete(throwOnAdd ? <int>[] : data);
    }
    if (throwOnAdd) {
340
      throw Exception('CompleterIOSink Error');
341
    }
342 343 344 345 346 347 348
    super.add(data);
  }
}

/// An IOSink that collects whatever is written to it.
class MemoryIOSink implements IOSink {
  @override
349
  Encoding encoding = utf8;
350 351 352 353 354 355 356 357 358

  final List<List<int>> writes = <List<int>>[];

  @override
  void add(List<int> data) {
    writes.add(data);
  }

  @override
359 360
  Future<void> addStream(Stream<List<int>> stream) {
    final Completer<void> completer = Completer<void>();
361 362 363 364 365
    StreamSubscription<List<int>> sub;
    sub = stream.listen(
      (List<int> data) {
        try {
          add(data);
366 367
        // Catches all exceptions to propagate them to the completer.
        } catch (err, stack) { // ignore: avoid_catches_without_on_clauses
368 369 370 371 372 373 374 375
          sub.cancel();
          completer.completeError(err, stack);
        }
      },
      onError: completer.completeError,
      onDone: completer.complete,
      cancelOnError: true,
    );
376 377 378 379 380 381 382 383 384 385 386 387 388 389
    return completer.future;
  }

  @override
  void writeCharCode(int charCode) {
    add(<int>[charCode]);
  }

  @override
  void write(Object obj) {
    add(encoding.encode('$obj'));
  }

  @override
390
  void writeln([ Object obj = '' ]) {
391 392 393 394
    add(encoding.encode('$obj\n'));
  }

  @override
395
  void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
396
    bool addSeparator = false;
397
    for (final dynamic object in objects) {
398 399 400 401 402 403 404 405 406
      if (addSeparator) {
        write(separator);
      }
      write(object);
      addSeparator = true;
    }
  }

  @override
407
  void addError(dynamic error, [ StackTrace stackTrace ]) {
408
    throw UnimplementedError();
409 410 411
  }

  @override
412
  Future<void> get done => close();
413 414

  @override
415
  Future<void> close() async { }
416 417

  @override
418
  Future<void> flush() async { }
419 420
}

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
class MemoryStdout extends MemoryIOSink implements io.Stdout {
  @override
  bool get hasTerminal => _hasTerminal;
  set hasTerminal(bool value) {
    assert(value != null);
    _hasTerminal = value;
  }
  bool _hasTerminal = true;

  @override
  io.IOSink get nonBlocking => this;

  @override
  bool get supportsAnsiEscapes => _supportsAnsiEscapes;
  set supportsAnsiEscapes(bool value) {
    assert(value != null);
    _supportsAnsiEscapes = value;
  }
  bool _supportsAnsiEscapes = true;

  @override
  int get terminalColumns {
443
    if (_terminalColumns != null) {
444
      return _terminalColumns;
445
    }
446 447 448 449 450 451 452
    throw const io.StdoutException('unspecified mock value');
  }
  set terminalColumns(int value) => _terminalColumns = value;
  int _terminalColumns;

  @override
  int get terminalLines {
453
    if (_terminalLines != null) {
454
      return _terminalLines;
455
    }
456 457 458 459 460 461
    throw const io.StdoutException('unspecified mock value');
  }
  set terminalLines(int value) => _terminalLines = value;
  int _terminalLines;
}

462 463
/// A Stdio that collects stdout and supports simulated stdin.
class MockStdio extends Stdio {
464
  final MemoryStdout _stdout = MemoryStdout();
465
  final MemoryIOSink _stderr = MemoryIOSink();
466
  final StreamController<List<int>> _stdin = StreamController<List<int>>();
467 468

  @override
469
  MemoryStdout get stdout => _stdout;
470

471
  @override
472
  MemoryIOSink get stderr => _stderr;
473

474 475 476 477
  @override
  Stream<List<int>> get stdin => _stdin.stream;

  void simulateStdin(String line) {
478
    _stdin.add(utf8.encode('$line\n'));
479 480
  }

481 482
  List<String> get writtenToStdout => _stdout.writes.map<String>(_stdout.encoding.decode).toList();
  List<String> get writtenToStderr => _stderr.writes.map<String>(_stderr.encoding.decode).toList();
483 484
}

485
class MockPollingDeviceDiscovery extends PollingDeviceDiscovery {
486 487
  MockPollingDeviceDiscovery() : super('mock');

488
  final List<Device> _devices = <Device>[];
489 490
  final StreamController<Device> _onAddedController = StreamController<Device>.broadcast();
  final StreamController<Device> _onRemovedController = StreamController<Device>.broadcast();
491 492

  @override
493 494 495 496 497 498
  Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
    lastPollingTimeout = timeout;
    return _devices;
  }

  Duration lastPollingTimeout;
499 500 501 502

  @override
  bool get supportsPlatform => true;

503 504 505
  @override
  bool get canListAnything => true;

506
  void addDevice(Device device) {
507 508 509 510
    _devices.add(device);
    _onAddedController.add(device);
  }

511 512 513 514 515 516 517 518 519 520 521
  void _removeDevice(Device device) {
    _devices.remove(device);
    _onRemovedController.add(device);
  }

  void setDevices(List<Device> devices) {
    while(_devices.isNotEmpty) {
      _removeDevice(_devices.first);
    }
    devices.forEach(addDevice);
  }
522 523 524 525 526 527 528 529

  @override
  Stream<Device> get onAdded => _onAddedController.stream;

  @override
  Stream<Device> get onRemoved => _onRemovedController.stream;
}

530
class MockIosProject extends Mock implements IosProject {
531
  static const String bundleId = 'com.example.test';
532
  static const String appBundleName = 'My Super Awesome App.app';
533

534
  @override
535
  Future<String> get productBundleIdentifier async => bundleId;
536 537

  @override
538
  Future<String> get hostAppBundleName async => appBundleName;
539 540
}

541
class MockAndroidDevice extends Mock implements AndroidDevice {
542
  @override
543
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm;
544 545

  @override
546
  bool isSupported() => true;
547

548 549 550 551 552 553
  @override
  bool get supportsHotRestart => true;

  @override
  bool get supportsFlutterExit => false;

554 555
  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;
556 557 558
}

class MockIOSDevice extends Mock implements IOSDevice {
559
  @override
560
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
561 562

  @override
563
  bool isSupported() => true;
564 565 566

  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;
567 568 569
}

class MockIOSSimulator extends Mock implements IOSSimulator {
570
  @override
571
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
572 573

  @override
574
  bool isSupported() => true;
575 576 577

  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;
578 579
}

580
void applyMocksToCommand(FlutterCommand command) {
581
  command.applicationPackages = MockApplicationPackageStore();
582
}
583

584 585
/// Common functionality for tracking mock interaction
class BasicMock {
586
  final List<String> messages = <String>[];
587

588
  void expectMessages(List<String> expectedMessages) {
589
    final List<String> actualMessages = List<String>.of(messages);
590 591 592 593
    messages.clear();
    expect(actualMessages, unorderedEquals(expectedMessages));
  }

594
  bool contains(String match) {
595 596
    print('Checking for `$match` in:');
    print(messages);
597
    final bool result = messages.contains(match);
598 599 600
    messages.clear();
    return result;
  }
601
}
602

603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
class MockDevFSOperations extends BasicMock implements DevFSOperations {
  Map<Uri, DevFSContent> devicePathToContent = <Uri, DevFSContent>{};

  @override
  Future<Uri> create(String fsName) async {
    messages.add('create $fsName');
    return Uri.parse('file:///$fsName');
  }

  @override
  Future<dynamic> destroy(String fsName) async {
    messages.add('destroy $fsName');
  }

  @override
  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
    String message = 'writeFile $fsName $deviceUri';
    if (content is DevFSFileContent) {
      message += ' ${content.file.path}';
    }
    messages.add(message);
    devicePathToContent[deviceUri] = content;
  }
}
627 628 629

class MockResidentCompiler extends BasicMock implements ResidentCompiler {
  @override
630
  void accept() { }
631 632

  @override
633
  Future<CompilerOutput> reject() async { return null; }
634 635

  @override
636
  void reset() { }
637 638

  @override
639
  Future<dynamic> shutdown() async { }
640 641 642 643 644 645 646 647

  @override
  Future<CompilerOutput> compileExpression(
    String expression,
    List<String> definitions,
    List<String> typeDefinitions,
    String libraryUri,
    String klass,
648
    bool isStatic,
649 650 651
  ) async {
    return null;
  }
652 653 654 655 656 657 658 659 660 661 662 663 664 665

  @override
  Future<CompilerOutput> compileExpressionToJs(
    String libraryUri,
    int line,
    int column,
    Map<String, String> jsModules,
    Map<String, String> jsFrameValues,
    String moduleName,
    String expression,
  ) async {
    return null;
  }

666
  @override
667
  Future<CompilerOutput> recompile(Uri mainPath, List<Uri> invalidatedFiles, { String outputPath, PackageConfig packageConfig }) async {
668 669
    globals.fs.file(outputPath).createSync(recursive: true);
    globals.fs.file(outputPath).writeAsStringSync('compiled_kernel_output');
670
    return CompilerOutput(outputPath, 0, <Uri>[]);
671
  }
672 673 674

  @override
  void addFileSystemRoot(String root) { }
675
}
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696

/// A fake implementation of [ProcessResult].
class FakeProcessResult implements ProcessResult {
  FakeProcessResult({
    this.exitCode = 0,
    this.pid = 1,
    this.stderr,
    this.stdout,
  });

  @override
  final int exitCode;

  @override
  final int pid;

  @override
  final dynamic stderr;

  @override
  final dynamic stdout;
697 698 699

  @override
  String toString() => stdout?.toString() ?? stderr?.toString() ?? runtimeType.toString();
700
}
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722

class MockStdIn extends Mock implements IOSink {
  final StringBuffer stdInWrites = StringBuffer();

  String getAndClear() {
    final String result = stdInWrites.toString();
    stdInWrites.clear();
    return result;
  }

  @override
  void write([ Object o = '' ]) {
    stdInWrites.write(o);
  }

  @override
  void writeln([ Object o = '' ]) {
    stdInWrites.writeln(o);
  }
}

class MockStream extends Mock implements Stream<List<int>> {}
Dan Field's avatar
Dan Field committed
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737

class AlwaysTrueBotDetector implements BotDetector {
  const AlwaysTrueBotDetector();

  @override
  Future<bool> get isRunningOnBot async => true;
}


class AlwaysFalseBotDetector implements BotDetector {
  const AlwaysFalseBotDetector();

  @override
  Future<bool> get isRunningOnBot async => false;
}