testbed.dart 5.86 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
// @dart = 2.8

7
import 'dart:async';
8
import 'dart:io';
9 10 11 12 13

import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
14
import 'package:flutter_tools/src/base/os.dart';
Dan Field's avatar
Dan Field committed
15
import 'package:flutter_tools/src/base/process.dart';
16
import 'package:flutter_tools/src/base/signals.dart';
17
import 'package:flutter_tools/src/base/terminal.dart';
18 19
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/context_runner.dart';
20
import 'package:flutter_tools/src/dart/pub.dart';
21
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
22
import 'package:flutter_tools/src/reporting/reporting.dart';
23
import 'package:flutter_tools/src/version.dart';
24

25
import 'context.dart';
26
import 'fake_http_client.dart';
27
import 'fakes.dart';
28
import 'throwing_pub.dart';
29

30 31
export 'package:flutter_tools/src/base/context.dart' show Generator;

32 33
// A default value should be provided if the vast majority of tests should use
// this provider. For example, [BufferLogger], [MemoryFileSystem].
34
final Map<Type, Generator> _testbedDefaults = <Type, Generator>{
35
  // Keeps tests fast by avoiding the actual file system.
36
  FileSystem: () => MemoryFileSystem(style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix),
37
  ProcessManager: () => FakeProcessManager.any(),
38 39 40 41
  Logger: () => BufferLogger(
    terminal: AnsiTerminal(stdio: globals.stdio, platform: globals.platform),  // Danger, using real stdio.
    outputPreferences: OutputPreferences.test(),
  ), // Allows reading logs and prevents stdout.
42
  OperatingSystemUtils: () => FakeOperatingSystemUtils(),
43
  OutputPreferences: () => OutputPreferences.test(), // configures BufferLogger to avoid color codes.
44
  Usage: () => TestUsage(), // prevent addition of analytics from burdening test mocks
45
  FlutterVersion: () => FakeFlutterVersion(), // prevent requirement to mock git for test runner.
46
  Signals: () => FakeSignals(),  // prevent registering actual signal handlers.
47
  Pub: () => ThrowingPub(), // prevent accidental invocations of pub.
48 49 50 51 52 53 54 55 56
};

/// Manages interaction with the tool injection and runner system.
///
/// The Testbed automatically injects reasonable defaults through the context
/// DI system such as a [BufferLogger] and a [MemoryFileSytem].
///
/// Example:
///
57
/// Testing that a filesystem operation works as expected:
58 59 60 61 62 63 64
///
///     void main() {
///       group('Example', () {
///         Testbed testbed;
///
///         setUp(() {
///           testbed = Testbed(setUp: () {
65
///             globals.fs.file('foo').createSync()
66 67 68
///           });
///         })
///
69
///         test('Can delete a file', () => testbed.run(() {
70 71 72
///           expect(globals.fs.file('foo').existsSync(), true);
///           globals.fs.file('foo').deleteSync();
///           expect(globals.fs.file('foo').existsSync(), false);
73 74 75 76 77 78 79 80 81 82 83
///         }));
///       });
///     }
///
/// For a more detailed example, see the code in test_compiler_test.dart.
class Testbed {
  /// Creates a new [TestBed]
  ///
  /// `overrides` provides more overrides in addition to the test defaults.
  /// `setup` may be provided to apply mocks within the tool managed zone,
  /// including any specified overrides.
84
  Testbed({FutureOr<void> Function() setup, Map<Type, Generator> overrides})
85 86
      : _setup = setup,
        _overrides = overrides;
87

88
  final FutureOr<void> Function() _setup;
89 90 91
  final Map<Type, Generator> _overrides;

  /// Runs `test` within a tool zone.
92 93 94
  ///
  /// `overrides` may be used to provide new context values for the single test
  /// case or override any context values from the setup.
95
  Future<T> run<T>(FutureOr<T> Function() test, {Map<Type, Generator> overrides}) {
96 97 98 99 100 101 102
    final Map<Type, Generator> testOverrides = <Type, Generator>{
      ..._testbedDefaults,
      // Add the initial setUp overrides
      ...?_overrides,
      // Add the test-specific overrides
      ...?overrides,
    };
Dan Field's avatar
Dan Field committed
103 104 105
    if (testOverrides.containsKey(ProcessUtils)) {
      throw StateError('Do not inject ProcessUtils for testing, use ProcessManager instead.');
    }
106 107
    // Cache the original flutter root to restore after the test case.
    final String originalFlutterRoot = Cache.flutterRoot;
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
    // Track pending timers to verify that they were correctly cleaned up.
    final Map<Timer, StackTrace> timers = <Timer, StackTrace>{};

    return HttpOverrides.runZoned(() {
      return runInContext<T>(() {
        return context.run<T>(
          name: 'testbed',
          overrides: testOverrides,
          zoneSpecification: ZoneSpecification(
            createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() timer) {
              final Timer result = parent.createTimer(zone, duration, timer);
              timers[result] = StackTrace.current;
              return result;
            },
            createPeriodicTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration period, void Function(Timer) timer) {
              final Timer result = parent.createPeriodicTimer(zone, period, timer);
              timers[result] = StackTrace.current;
              return result;
126
            },
127 128 129 130 131 132 133 134
          ),
          body: () async {
            Cache.flutterRoot = '';
            if (_setup != null) {
              await _setup();
            }
            await test();
            Cache.flutterRoot = originalFlutterRoot;
135
            for (final MapEntry<Timer, StackTrace> entry in timers.entries) {
136 137 138 139 140 141 142
              if (entry.key.isActive) {
                throw StateError('A Timer was active at the end of a test: ${entry.value}');
              }
            }
            return null;
          });
      });
143
    }, createHttpClient: (SecurityContext c) => FakeHttpClient.any());
144
  }
145
}