Unverified Commit 8d11f5c7 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Make AppContext immutable and race-free (#15984)

This updates AppContext per the recommendations in #15352

Fixes #15352
parent 4226bff7
......@@ -12,8 +12,10 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/context_runner.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/disabled_usage.dart';
import 'package:flutter_tools/src/flx.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/usage.dart';
const String _kOptionPackages = 'packages';
const String _kOptionWorking = 'working-dir';
......@@ -25,8 +27,10 @@ const List<String> _kRequiredOptions = const <String>[
_kOptionAssetManifestOut,
];
Future<Null> main(List<String> args) async {
await runInContext(args, run);
Future<Null> main(List<String> args) {
return runInContext<Null>(() => run(args), overrides: <Type, dynamic>{
Usage: new DisabledUsage(),
});
}
Future<Null> writeFile(libfs.File outputFile, DevFSContent content) async {
......
......@@ -12,8 +12,10 @@ import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/context_runner.dart';
import 'package:flutter_tools/src/disabled_usage.dart';
import 'package:flutter_tools/src/flx.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/usage.dart';
const String _kOptionPackages = 'packages';
const String _kOptionOutput = 'output-file';
......@@ -33,8 +35,10 @@ const List<String> _kRequiredOptions = const <String>[
_kOptionBuildRoot,
];
Future<Null> main(List<String> args) async {
await runInContext(args, run);
Future<Null> main(List<String> args) {
return runInContext<Null>(() => run(args), overrides: <Type, dynamic>{
Usage: new DisabledUsage(),
});
}
Future<Null> run(List<String> args) async {
......
......@@ -6,24 +6,18 @@ import 'dart:async';
import 'package:args/args.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/context_runner.dart';
import 'package:flutter_tools/src/dart/package_map.dart';
import 'package:flutter_tools/src/disabled_usage.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/test/flutter_platform.dart' as loader;
import 'package:flutter_tools/src/usage.dart';
import 'package:process/process.dart';
import 'package:test/src/executable.dart'
as test; // ignore: implementation_imports
// Note: this was largely inspired by lib/src/commands/test.dart.
const String _kOptionPackages = 'packages';
......@@ -35,22 +29,9 @@ const List<String> _kRequiredOptions = const <String>[
_kOptionTestDirectory,
];
Future<Null> main(List<String> args) async {
final AppContext executableContext = new AppContext();
executableContext.setVariable(Logger, new StdoutLogger());
await executableContext.runInZone(() {
// Initialize the context with some defaults.
// This list must be kept in sync with lib/executable.dart.
context.putIfAbsent(Stdio, () => const Stdio());
context.putIfAbsent(Platform, () => const LocalPlatform());
context.putIfAbsent(FileSystem, () => const LocalFileSystem());
context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
context.putIfAbsent(Logger, () => new StdoutLogger());
context.putIfAbsent(Cache, () => new Cache());
context.putIfAbsent(Config, () => new Config());
context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils());
context.putIfAbsent(Usage, () => new DisabledUsage());
return run(args);
Future<Null> main(List<String> args) {
return runInContext<Null>(() => run(args), overrides: <Type, dynamic>{
Usage: new DisabledUsage(),
});
}
......
......@@ -7,26 +7,18 @@ import 'dart:async';
import 'package:args/command_runner.dart';
import 'package:intl/intl_standalone.dart' as intl;
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'src/artifacts.dart';
import 'src/base/common.dart';
import 'src/base/config.dart';
import 'src/base/context.dart';
import 'src/base/file_system.dart';
import 'src/base/io.dart';
import 'src/base/logger.dart';
import 'src/base/platform.dart';
import 'src/base/process.dart';
import 'src/base/utils.dart';
import 'src/cache.dart';
import 'src/context_runner.dart';
import 'src/crash_reporting.dart';
import 'src/devfs.dart';
import 'src/device.dart';
import 'src/doctor.dart';
import 'src/globals.dart';
import 'src/ios/simulators.dart';
import 'src/run_hot.dart';
import 'src/runner/flutter_command.dart';
import 'src/runner/flutter_command_runner.dart';
import 'src/usage.dart';
......@@ -41,7 +33,7 @@ Future<int> run(
bool verboseHelp: false,
bool reportCrashes,
String flutterVersion,
}) async {
}) {
reportCrashes ??= !isRunningOnBot;
if (muteCommandLogging) {
......@@ -54,35 +46,7 @@ Future<int> run(
final FlutterCommandRunner runner = new FlutterCommandRunner(verboseHelp: verboseHelp);
commands.forEach(runner.addCommand);
// Construct a context.
final AppContext _executableContext = new AppContext();
// Make the context current.
return await _executableContext.runInZone(() async {
// Initialize the context with some defaults.
// NOTE: Similar lists also exist in `bin/fuchsia_builder.dart` and
// `test/src/context.dart`. If you update this list of defaults, look
// in those locations as well to see if you need a similar update there.
// Seed these context entries first since others depend on them
context.putIfAbsent(Stdio, () => const Stdio());
context.putIfAbsent(Platform, () => const LocalPlatform());
context.putIfAbsent(FileSystem, () => const LocalFileSystem());
context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger());
context.putIfAbsent(Config, () => new Config());
// Order-independent context entries
context.putIfAbsent(BotDetector, () => const BotDetector());
context.putIfAbsent(DeviceManager, () => new DeviceManager());
context.putIfAbsent(DevFSConfig, () => new DevFSConfig());
context.putIfAbsent(Doctor, () => new Doctor());
context.putIfAbsent(HotRunnerConfig, () => new HotRunnerConfig());
context.putIfAbsent(Cache, () => new Cache());
context.putIfAbsent(Artifacts, () => new CachedArtifacts());
context.putIfAbsent(IOSSimulatorUtils, () => new IOSSimulatorUtils());
context.putIfAbsent(SimControl, () => new SimControl());
return runInContext<int>(() async {
// Initialize the system locale.
await intl.findSystemLocale();
......@@ -104,6 +68,7 @@ typedef void WriteCallback([String string]);
/// Writes a line to STDERR.
///
/// Overwrite this in tests to avoid spurious test output.
// TODO(tvolkert): Remove this in favor of context[Stdio]
@visibleForTesting
WriteCallback writelnStderr = stderr.writeln;
......@@ -230,11 +195,13 @@ Future<File> _createLocalCrashReport(List<String> args, dynamic error, StackTrac
Future<String> _doctorText() async {
try {
final BufferLogger logger = new BufferLogger();
final AppContext appContext = new AppContext();
appContext.setVariable(Logger, logger);
await appContext.runInZone(() => doctor.diagnose(verbose: true));
await context.run<Future<bool>>(
body: () => doctor.diagnose(verbose: true),
overrides: <Type, Generator>{
Logger: () => logger,
},
);
return logger.statusText;
} catch (error, trace) {
......
......@@ -12,8 +12,7 @@ import '../base/version.dart';
import '../globals.dart';
import '../ios/plist_utils.dart';
AndroidStudio get androidStudio =>
context.putIfAbsent(AndroidStudio, AndroidStudio.latestValid);
AndroidStudio get androidStudio => context[AndroidStudio];
// Android Studio layout:
......
......@@ -17,7 +17,7 @@ import '../doctor.dart';
import '../globals.dart';
import 'android_sdk.dart';
AndroidWorkflow get androidWorkflow => context.putIfAbsent(AndroidWorkflow, () => new AndroidWorkflow());
AndroidWorkflow get androidWorkflow => context[AndroidWorkflow];
enum LicensesAccepted {
none,
......
......@@ -84,8 +84,8 @@ class EngineBuildPaths {
abstract class Artifacts {
static Artifacts get instance => context[Artifacts];
static void useLocalEngine(String engineSrcPath, EngineBuildPaths engineBuildPaths) {
context.setVariable(Artifacts, new LocalEngineArtifacts(engineSrcPath, engineBuildPaths.targetEngine, engineBuildPaths.hostEngine));
static LocalEngineArtifacts getLocalEngine(String engineSrcPath, EngineBuildPaths engineBuildPaths) {
return new LocalEngineArtifacts(engineSrcPath, engineBuildPaths.targetEngine, engineBuildPaths.hostEngine);
}
// Returns the requested [artifact] for the [platform] and [mode] combination.
......
......@@ -21,9 +21,9 @@ const AssetBundleFactory _kManifestFactory = const _ManifestAssetBundleFactory()
/// Injected factory class for spawning [AssetBundle] instances.
abstract class AssetBundleFactory {
/// The singleton instance, pulled from the [AppContext].
static AssetBundleFactory get instance => context == null
? _kManifestFactory
: context.putIfAbsent(AssetBundleFactory, () => _kManifestFactory);
static AssetBundleFactory get instance => context[AssetBundleFactory];
static AssetBundleFactory get defaultInstance => _kManifestFactory;
/// Creates a new [AssetBundle].
AssetBundle createBundle();
......
......@@ -17,7 +17,7 @@ import 'context.dart';
import 'file_system.dart';
import 'process.dart';
GenSnapshot get genSnapshot => context.putIfAbsent(GenSnapshot, () => const GenSnapshot());
GenSnapshot get genSnapshot => context[GenSnapshot];
/// A snapshot build configuration.
class SnapshotType {
......
......@@ -3,79 +3,182 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
typedef void ErrorHandler(dynamic error, StackTrace stackTrace);
import 'package:meta/meta.dart';
/// A singleton for application functionality. This singleton can be different
/// on a per-Zone basis.
AppContext get context => Zone.current['context'];
/// Generates an [AppContext] value.
///
/// Generators are allowed to return `null`, in which case the context will
/// store the `null` value as the value for that type.
typedef dynamic Generator();
class AppContext {
final Map<Type, dynamic> _instances = <Type, dynamic>{};
Zone _zone;
AppContext() : _zone = Zone.current;
/// An exception thrown by [AppContext] when you try to get a [Type] value from
/// the context, and the instantiation of the value results in a dependency
/// cycle.
class ContextDependencyCycleException implements Exception {
ContextDependencyCycleException._(this.cycle);
bool isSet(Type type) {
if (_instances.containsKey(type))
return true;
/// The dependency cycle (last item depends on first item).
final List<Type> cycle;
final AppContext parent = _calcParent(_zone);
return parent != null ? parent.isSet(type) : false;
}
@override
String toString() => 'Dependency cycle detected: ${cycle.join(' -> ')}';
}
dynamic getVariable(Type type) {
if (_instances.containsKey(type))
return _instances[type];
/// The current [AppContext], as determined by the [Zone] hierarchy.
///
/// This will be the first context found as we scan up the zone hierarchy, or
/// the "root" context if a context cannot be found in the hierarchy. The root
/// context will not have any values associated with it.
///
/// This is guaranteed to never return `null`.
AppContext get context => Zone.current[_Key.key] ?? AppContext._root;
/// A lookup table (mapping types to values) and an implied scope, in which
/// code is run.
///
/// [AppContext] is used to define a singleton injection context for code that
/// is run within it. Each time you call [run], a child context (and a new
/// scope) is created.
///
/// Child contexts are created and run using zones. To read more about how
/// zones work, see https://www.dartlang.org/articles/libraries/zones.
class AppContext {
AppContext._(
this._parent,
this.name, [
this._overrides = const <Type, Generator>{},
this._fallbacks = const <Type, Generator>{},
]);
final String name;
final AppContext _parent;
final Map<Type, Generator> _overrides;
final Map<Type, Generator> _fallbacks;
final Map<Type, dynamic> _values = <Type, dynamic>{};
List<Type> _reentrantChecks;
/// Bootstrap context.
static final AppContext _root = new AppContext._(null, 'ROOT');
dynamic _boxNull(dynamic value) => value ?? _BoxedNull.instance;
dynamic _unboxNull(dynamic value) => value == _BoxedNull.instance ? null : value;
/// Returns the generated value for [type] if such a generator exists.
///
/// If [generators] does not contain a mapping for the specified [type], this
/// returns `null`.
///
/// If a generator existed and generated a `null` value, this will return a
/// boxed value indicating null.
///
/// If a value for [type] has already been generated by this context, the
/// existing value will be returned, and the generator will not be invoked.
///
/// If the generator ends up triggering a reentrant call, it signals a
/// dependency cycle, and a [ContextDependencyCycleException] will be thrown.
dynamic _generateIfNecessary(Type type, Map<Type, Generator> generators) {
if (!generators.containsKey(type))
return null;
final AppContext parent = _calcParent(_zone);
return parent?.getVariable(type);
return _values.putIfAbsent(type, () {
_reentrantChecks ??= <Type>[];
final int index = _reentrantChecks.indexOf(type);
if (index >= 0) {
// We're already in the process of trying to generate this type.
throw new ContextDependencyCycleException._(
new UnmodifiableListView<Type>(_reentrantChecks.sublist(index)));
}
_reentrantChecks.add(type);
try {
return _boxNull(generators[type]());
} finally {
_reentrantChecks.removeLast();
if (_reentrantChecks.isEmpty)
_reentrantChecks = null;
}
});
}
void setVariable(Type type, dynamic instance) {
_instances[type] = instance;
/// Gets the value associated with the specified [type], or `null` if no
/// such value has been associated.
dynamic operator [](Type type) {
dynamic value = _generateIfNecessary(type, _overrides);
if (value == null && _parent != null)
value = _parent[type];
return _unboxNull(value ?? _generateIfNecessary(type, _fallbacks));
}
dynamic operator[](Type type) => getVariable(type);
/// Runs [body] in a child context and returns the value returned by [body].
///
/// If [overrides] is specified, the child context will return corresponding
/// values when consulted via [operator[]].
///
/// If [fallbacks] is specified, the child context will return corresponding
/// values when consulted via [operator[]] only if its parent context didn't
/// return such a value.
///
/// If [name] is specified, the child context will be assigned the given
/// name. This is useful for debugging purposes and is analogous to naming a
/// thread in Java.
V run<V>({
@required V body(),
String name,
Map<Type, Generator> overrides,
Map<Type, Generator> fallbacks,
}) {
final AppContext child = new AppContext._(
this,
name,
new Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}),
new Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}),
);
return runZoned<V>(
body,
zoneValues: <_Key, AppContext>{_Key.key: child},
);
}
dynamic putIfAbsent(Type type, dynamic ifAbsent()) {
dynamic value = getVariable(type);
if (value != null) {
return value;
@override
String toString() {
final StringBuffer buf = new StringBuffer();
String indent = '';
AppContext ctx = this;
while (ctx != null) {
buf.write('AppContext');
if (ctx.name != null)
buf.write('[${ctx.name}]');
if (ctx._overrides.isNotEmpty)
buf.write('\n$indent overrides: [${ctx._overrides.keys.join(', ')}]');
if (ctx._fallbacks.isNotEmpty)
buf.write('\n$indent fallbacks: [${ctx._fallbacks.keys.join(', ')}]');
if (ctx._parent != null)
buf.write('\n$indent parent: ');
ctx = ctx._parent;
indent += ' ';
}
value = ifAbsent();
setVariable(type, value);
return value;
return buf.toString();
}
}
AppContext _calcParent(Zone zone) {
final Zone parentZone = zone.parent;
if (parentZone == null)
return null;
/// Private key used to store the [AppContext] in the [Zone].
class _Key {
const _Key();
final AppContext parentContext = parentZone['context'];
return parentContext == this
? _calcParent(parentZone)
: parentContext;
}
static const _Key key = const _Key();
Future<dynamic> runInZone(dynamic method(), {
ZoneBinaryCallback<dynamic, dynamic, StackTrace> onError
}) {
return runZoned(
() => _run(method),
zoneValues: <String, dynamic>{ 'context': this },
onError: onError
);
}
@override
String toString() => 'context';
}
Future<dynamic> _run(dynamic method()) async {
final Zone previousZone = _zone;
try {
_zone = Zone.current;
return await method();
} finally {
_zone = previousZone;
}
}
/// Private object that denotes a generated `null` value.
class _BoxedNull {
const _BoxedNull();
static const _BoxedNull instance = const _BoxedNull();
}
......@@ -22,19 +22,15 @@ const FileSystem _kLocalFs = const LocalFileSystem();
///
/// By default it uses local disk-based implementation. Override this in tests
/// with [MemoryFileSystem].
FileSystem get fs => context == null ? _kLocalFs : context[FileSystem];
FileSystem get fs => context[FileSystem] ?? _kLocalFs;
/// Enables recording of file system activity to the specified base recording
/// [location].
///
/// This sets the [active file system](fs) to one that records all invocation
/// activity before delegating to a [LocalFileSystem].
/// Gets a [FileSystem] that will record file system activity to the specified
/// base recording [location].
///
/// Activity will be recorded in a subdirectory of [location] named `"file"`.
/// It is permissible for [location] to represent an existing non-empty
/// directory as long as there is no collision with the `"file"` subdirectory.
void enableRecordingFileSystem(String location) {
final FileSystem originalFileSystem = fs;
RecordingFileSystem getRecordingFileSystem(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
final RecordingFileSystem fileSystem = new RecordingFileSystem(
delegate: _kLocalFs, destination: dir);
......@@ -42,22 +38,19 @@ void enableRecordingFileSystem(String location) {
await fileSystem.recording.flush(
pendingResultTimeout: const Duration(seconds: 5),
);
context.setVariable(FileSystem, originalFileSystem);
}, ShutdownStage.SERIALIZE_RECORDING);
context.setVariable(FileSystem, fileSystem);
return fileSystem;
}
/// Enables file system replay mode.
///
/// This sets the [active file system](fs) to one that replays invocation
/// activity from a previously recorded set of invocations.
/// Gets a [FileSystem] that replays invocation activity from a previously
/// recorded set of invocations.
///
/// [location] must represent a directory to which file system activity has
/// been recorded (i.e. the result of having been previously passed to
/// [enableRecordingFileSystem]), or a [ToolExit] will be thrown.
void enableReplayFileSystem(String location) {
/// [getRecordingFileSystem]), or a [ToolExit] will be thrown.
ReplayFileSystem getReplayFileSystem(String location) {
final Directory dir = getReplaySource(location, _kRecordingType);
context.setVariable(FileSystem, new ReplayFileSystem(recording: dir));
return new ReplayFileSystem(recording: dir);
}
/// Create the ancestor directories of a file path if they do not already exist.
......
......@@ -8,7 +8,7 @@ import 'context.dart';
/// command-line flags and options that were specified during the invocation of
/// the Flutter tool.
Flags get flags => context?.getVariable(Flags) ?? const _EmptyFlags();
Flags get flags => context[Flags];
/// Encapsulation of the command-line flags and options that were specified
/// during the invocation of the Flutter tool.
......@@ -52,8 +52,8 @@ class Flags {
}
}
class _EmptyFlags implements Flags {
const _EmptyFlags();
class EmptyFlags implements Flags {
const EmptyFlags();
@override
ArgResults get _globalResults => null;
......
......@@ -155,23 +155,8 @@ class Stdio {
io.IOSink get stderr => io.stderr;
}
io.IOSink get stderr {
if (context == null)
return io.stderr;
final Stdio contextStreams = context[Stdio];
return contextStreams.stderr;
}
io.IOSink get stderr => context[Stdio].stderr;
Stream<List<int>> get stdin {
if (context == null)
return io.stdin;
final Stdio contextStreams = context[Stdio];
return contextStreams.stdin;
}
Stream<List<int>> get stdin => context[Stdio].stdin;
io.IOSink get stdout {
if (context == null)
return io.stdout;
final Stdio contextStreams = context[Stdio];
return contextStreams.stdout;
}
io.IOSink get stdout => context[Stdio].stdout;
......@@ -11,7 +11,7 @@ import 'process.dart';
import 'process_manager.dart';
/// Returns [OperatingSystemUtils] active in the current app context (i.e. zone).
OperatingSystemUtils get os => context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils());
OperatingSystemUtils get os => context[OperatingSystemUtils];
abstract class OperatingSystemUtils {
factory OperatingSystemUtils() {
......
......@@ -14,26 +14,29 @@ export 'package:platform/platform.dart';
const Platform _kLocalPlatform = const LocalPlatform();
const String _kRecordingType = 'platform';
Platform get platform => context == null ? _kLocalPlatform : context[Platform];
Platform get platform => context[Platform] ?? _kLocalPlatform;
/// Enables serialization of the current [platform] to the specified base
/// recording [location].
/// Serializes the current [platform] to the specified base recording
/// [location].
///
/// Platform metadata will be recorded in a subdirectory of [location] named
/// `"platform"`. It is permissible for [location] to represent an existing
/// non-empty directory as long as there is no collision with the `"platform"`
/// subdirectory.
Future<Null> enableRecordingPlatform(String location) async {
///
/// Returns the existing platform.
Future<Platform> getRecordingPlatform(String location) async {
final Directory dir = getRecordingSink(location, _kRecordingType);
final File file = _getPlatformManifest(dir);
await file.writeAsString(platform.toJson(), flush: true);
return platform;
}
Future<Null> enableReplayPlatform(String location) async {
Future<FakePlatform> getReplayPlatform(String location) async {
final Directory dir = getReplaySource(location, _kRecordingType);
final File file = _getPlatformManifest(dir);
final String json = await file.readAsString();
context.setVariable(Platform, new FakePlatform.fromJson(json));
return new FakePlatform.fromJson(json);
}
File _getPlatformManifest(Directory dir) {
......
......@@ -7,14 +7,9 @@ import 'dart:async';
import 'context.dart';
import 'io.dart';
const PortScanner _kLocalPortScanner = const HostPortScanner();
const int _kMaxSearchIterations = 20;
PortScanner get portScanner {
return context == null
? _kLocalPortScanner
: context.putIfAbsent(PortScanner, () => _kLocalPortScanner);
}
PortScanner get portScanner => context[PortScanner];
abstract class PortScanner {
const PortScanner();
......
......@@ -16,43 +16,32 @@ const String _kRecordingType = 'process';
const ProcessManager _kLocalProcessManager = const LocalProcessManager();
/// The active process manager.
ProcessManager get processManager {
return context == null
? _kLocalProcessManager
: context[ProcessManager];
}
ProcessManager get processManager => context[ProcessManager] ?? _kLocalProcessManager;
/// Enables recording of process invocation activity to the specified base
/// recording [location].
///
/// This sets the [active process manager](processManager) to one that records
/// all process activity before delegating to a [LocalProcessManager].
/// Gets a [ProcessManager] that will record process invocation activity to the
/// specified base recording [location].
///
/// Activity will be recorded in a subdirectory of [location] named `"process"`.
/// It is permissible for [location] to represent an existing non-empty
/// directory as long as there is no collision with the `"process"`
/// subdirectory.
void enableRecordingProcessManager(String location) {
final ProcessManager originalProcessManager = processManager;
RecordingProcessManager getRecordingProcessManager(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
const ProcessManager delegate = const LocalProcessManager();
final RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
addShutdownHook(() async {
await manager.flush(finishRunningProcesses: true);
context.setVariable(ProcessManager, originalProcessManager);
}, ShutdownStage.SERIALIZE_RECORDING);
context.setVariable(ProcessManager, manager);
return manager;
}
/// Enables process invocation replay mode.
///
/// This sets the [active process manager](processManager) to one that replays
/// process activity from a previously recorded set of invocations.
/// Gets a [ProcessManager] that replays process activity from a previously
/// recorded set of invocations.
///
/// [location] must represent a directory to which process activity has been
/// recorded (i.e. the result of having been previously passed to
/// [enableRecordingProcessManager]), or a [ToolExit] will be thrown.
Future<Null> enableReplayProcessManager(String location) async {
/// [getRecordingProcessManager]), or a [ToolExit] will be thrown.
Future<ReplayProcessManager> getReplayProcessManager(String location) async {
final Directory dir = getReplaySource(location, _kRecordingType);
ProcessManager manager;
......@@ -69,5 +58,5 @@ Future<Null> enableReplayProcessManager(String location) async {
throwToolExit('Invalid replay-from: $error');
}
context.setVariable(ProcessManager, manager);
return manager;
}
......@@ -45,7 +45,7 @@ class BotDetector {
}
bool get isRunningOnBot {
final BotDetector botDetector = context?.getVariable(BotDetector) ?? _kBotDetector;
final BotDetector botDetector = context[BotDetector] ?? _kBotDetector;
return botDetector.isRunningOnBot;
}
......@@ -231,7 +231,7 @@ class Uuid {
value.toRadixString(16).padLeft(count, '0');
}
Clock get clock => context.putIfAbsent(Clock, () => const Clock());
Clock get clock => context[Clock];
typedef Future<Null> AsyncCallback();
......
......@@ -50,21 +50,24 @@ class DaemonCommand extends FlutterCommand {
Future<Null> runCommand() {
printStatus('Starting device daemon...');
final AppContext appContext = new AppContext();
final NotifyingLogger notifyingLogger = new NotifyingLogger();
appContext.setVariable(Logger, notifyingLogger);
Cache.releaseLockEarly();
return appContext.runInZone(() async {
final Daemon daemon = new Daemon(
stdinCommandStream, stdoutCommandResponse,
daemonCommand: this, notifyingLogger: notifyingLogger);
final int code = await daemon.onExit;
if (code != 0)
throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
});
return context.run<Future<Null>>(
body: () async {
final Daemon daemon = new Daemon(
stdinCommandStream, stdoutCommandResponse,
daemonCommand: this, notifyingLogger: notifyingLogger);
final int code = await daemon.onExit;
if (code != 0)
throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
},
overrides: <Type, Generator>{
Logger: () => notifyingLogger,
},
);
}
}
......@@ -810,10 +813,12 @@ class AppInstance {
dynamic _runInZone(AppDomain domain, dynamic method()) {
_logger ??= new _AppRunLogger(domain, this, parent: logToStdout ? logger : null);
final AppContext appContext = new AppContext();
appContext.setVariable(Logger, _logger);
appContext.setVariable(Stdio, const Stdio());
return appContext.runInZone(method);
return context.run<dynamic>(
body: method,
overrides: <Type, Generator>{
Logger: () => _logger,
},
);
}
}
......
......@@ -4,38 +4,73 @@
import 'dart:async';
import 'package:process/process.dart';
import 'package:quiver/time.dart';
import 'android/android_sdk.dart';
import 'android/android_studio.dart';
import 'android/android_workflow.dart';
import 'artifacts.dart';
import 'asset.dart';
import 'base/build.dart';
import 'base/config.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/flags.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/port_scanner.dart';
import 'base/utils.dart';
import 'cache.dart';
import 'disabled_usage.dart';
import 'devfs.dart';
import 'device.dart';
import 'doctor.dart';
import 'ios/cocoapods.dart';
import 'ios/ios_workflow.dart';
import 'ios/mac.dart';
import 'ios/simulators.dart';
import 'ios/xcodeproj.dart';
import 'run_hot.dart';
import 'usage.dart';
import 'version.dart';
typedef Future<Null> Runner(List<String> args);
Future<Null> runInContext(List<String> args, Runner runner) {
final AppContext executableContext = new AppContext();
executableContext.setVariable(Logger, new StdoutLogger());
return executableContext.runInZone(() {
// Initialize the context with some defaults.
// This list must be kept in sync with lib/executable.dart.
context.putIfAbsent(BotDetector, () => const BotDetector());
context.putIfAbsent(Stdio, () => const Stdio());
context.putIfAbsent(Platform, () => const LocalPlatform());
context.putIfAbsent(FileSystem, () => const LocalFileSystem());
context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
context.putIfAbsent(Logger, () => new StdoutLogger());
context.putIfAbsent(Cache, () => new Cache());
context.putIfAbsent(Config, () => new Config());
context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils());
context.putIfAbsent(Usage, () => new DisabledUsage());
return runner(args);
});
Future<T> runInContext<T>(
FutureOr<T> runner(), {
Map<Type, dynamic> overrides,
}) async {
return await context.run<Future<T>>(
name: 'global fallbacks',
body: () async => await runner(),
overrides: overrides,
fallbacks: <Type, Generator>{
AndroidSdk: AndroidSdk.locateAndroidSdk,
AndroidStudio: AndroidStudio.latestValid,
AndroidWorkflow: () => new AndroidWorkflow(),
Artifacts: () => new CachedArtifacts(),
AssetBundleFactory: () => AssetBundleFactory.defaultInstance,
BotDetector: () => const BotDetector(),
Cache: () => new Cache(),
Clock: () => const Clock(),
CocoaPods: () => const CocoaPods(),
Config: () => new Config(),
DevFSConfig: () => new DevFSConfig(),
DeviceManager: () => new DeviceManager(),
Doctor: () => new Doctor(),
Flags: () => const EmptyFlags(),
FlutterVersion: () => new FlutterVersion(const Clock()),
GenSnapshot: () => const GenSnapshot(),
HotRunnerConfig: () => new HotRunnerConfig(),
IMobileDevice: () => const IMobileDevice(),
IOSSimulatorUtils: () => new IOSSimulatorUtils(),
IOSWorkflow: () => const IOSWorkflow(),
Logger: () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger(),
OperatingSystemUtils: () => new OperatingSystemUtils(),
PortScanner: () => const HostPortScanner(),
SimControl: () => new SimControl(),
Stdio: () => const Stdio(),
Usage: () => new Usage(),
Xcode: () => new Xcode(),
XcodeProjectInterpreter: () => new XcodeProjectInterpreter(),
},
);
}
......@@ -197,7 +197,7 @@ enum ValidationType {
}
abstract class DoctorValidator {
DoctorValidator(this.title);
const DoctorValidator(this.title);
final String title;
......
......@@ -31,7 +31,7 @@ const String cocoaPodsUpgradeInstructions = '''
brew upgrade cocoapods
pod setup''';
CocoaPods get cocoaPods => context.putIfAbsent(CocoaPods, () => const CocoaPods());
CocoaPods get cocoaPods => context[CocoaPods];
class CocoaPods {
const CocoaPods();
......
......@@ -13,10 +13,10 @@ import '../doctor.dart';
import 'cocoapods.dart';
import 'mac.dart';
IOSWorkflow get iosWorkflow => context.putIfAbsent(IOSWorkflow, () => new IOSWorkflow());
IOSWorkflow get iosWorkflow => context[IOSWorkflow];
class IOSWorkflow extends DoctorValidator implements Workflow {
IOSWorkflow() : super('iOS toolchain - develop for iOS devices');
const IOSWorkflow() : super('iOS toolchain - develop for iOS devices');
@override
bool get appliesToHostPlatform => platform.isMacOS;
......
......@@ -35,9 +35,9 @@ const int kXcodeRequiredVersionMinor = 0;
// Homebrew.
const PythonModule kPythonSix = const PythonModule('six');
IMobileDevice get iMobileDevice => context.putIfAbsent(IMobileDevice, () => const IMobileDevice());
IMobileDevice get iMobileDevice => context[IMobileDevice];
Xcode get xcode => context.putIfAbsent(Xcode, () => new Xcode());
Xcode get xcode => context[Xcode];
class PythonModule {
const PythonModule(this.name);
......
......@@ -85,10 +85,7 @@ void updateGeneratedXcodeProperties({
localsFile.writeAsStringSync(localsBuffer.toString());
}
XcodeProjectInterpreter get xcodeProjectInterpreter => context.putIfAbsent(
XcodeProjectInterpreter,
() => new XcodeProjectInterpreter(),
);
XcodeProjectInterpreter get xcodeProjectInterpreter => context[XcodeProjectInterpreter];
/// Interpreter of Xcode projects.
class XcodeProjectInterpreter {
......
......@@ -222,46 +222,49 @@ abstract class FlutterCommand extends Command<Null> {
/// and [runCommand] to execute the command
/// so that this method can record and report the overall time to analytics.
@override
Future<Null> run() async {
Future<Null> run() {
final DateTime startTime = clock.now();
context.setVariable(FlutterCommand, this);
if (flutterUsage.isFirstRun)
flutterUsage.printWelcome();
FlutterCommandResult commandResult;
try {
commandResult = await verifyThenRunCommand();
} on ToolExit {
commandResult = const FlutterCommandResult(ExitStatus.fail);
rethrow;
} finally {
final DateTime endTime = clock.now();
printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
if (usagePath != null) {
final List<String> labels = <String>[];
if (commandResult?.exitStatus != null)
labels.add(getEnumName(commandResult.exitStatus));
if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
labels.addAll(commandResult.timingLabelParts);
final String label = labels
.where((String label) => !isBlank(label))
.join('-');
flutterUsage.sendTiming(
'flutter',
name,
// If the command provides its own end time, use it. Otherwise report
// the duration of the entire execution.
(commandResult?.endTimeOverride ?? endTime).difference(startTime),
// Report in the form of `success-[parameter1-parameter2]`, all of which
// can be null if the command doesn't provide a FlutterCommandResult.
label: label == '' ? null : label,
);
}
}
return context.run<Future<Null>>(
name: 'command',
overrides: <Type, Generator>{FlutterCommand: () => this},
body: () async {
if (flutterUsage.isFirstRun)
flutterUsage.printWelcome();
FlutterCommandResult commandResult;
try {
commandResult = await verifyThenRunCommand();
} on ToolExit {
commandResult = const FlutterCommandResult(ExitStatus.fail);
rethrow;
} finally {
final DateTime endTime = clock.now();
printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
if (usagePath != null) {
final List<String> labels = <String>[];
if (commandResult?.exitStatus != null)
labels.add(getEnumName(commandResult.exitStatus));
if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
labels.addAll(commandResult.timingLabelParts);
final String label = labels
.where((String label) => !isBlank(label))
.join('-');
flutterUsage.sendTiming(
'flutter',
name,
// If the command provides its own end time, use it. Otherwise report
// the duration of the entire execution.
(commandResult?.endTimeOverride ?? endTime).difference(startTime),
// Report in the form of `success-[parameter1-parameter2]`, all of which
// can be null if the command doesn't provide a FlutterCommandResult.
label: label == '' ? null : label,
);
}
}
},
);
}
/// Perform validation then call [runCommand] to execute the command.
......
......@@ -7,8 +7,10 @@ import 'dart:convert';
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import '../android/android_sdk.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/context.dart';
......@@ -168,12 +170,14 @@ class FlutterCommandRunner extends CommandRunner<Null> {
@override
Future<Null> runCommand(ArgResults topLevelResults) async {
context.setVariable(Flags, new Flags(topLevelResults));
final Map<Type, dynamic> contextOverrides = <Type, dynamic>{
Flags: new Flags(topLevelResults),
};
// Check for verbose.
if (topLevelResults['verbose']) {
// Override the logger.
context.setVariable(Logger, new VerboseLogger(context[Logger]));
contextOverrides[Logger] = new VerboseLogger(logger);
}
String recordTo = topLevelResults['record-to'];
......@@ -214,9 +218,11 @@ class FlutterCommandRunner extends CommandRunner<Null> {
recordTo = recordTo.trim();
if (recordTo.isEmpty)
throwToolExit('record-to location not specified');
enableRecordingProcessManager(recordTo);
enableRecordingFileSystem(recordTo);
await enableRecordingPlatform(recordTo);
contextOverrides.addAll(<Type, dynamic>{
ProcessManager: getRecordingProcessManager(recordTo),
FileSystem: getRecordingFileSystem(recordTo),
Platform: await getRecordingPlatform(recordTo),
});
VMService.enableRecordingConnection(recordTo);
}
......@@ -224,66 +230,74 @@ class FlutterCommandRunner extends CommandRunner<Null> {
replayFrom = replayFrom.trim();
if (replayFrom.isEmpty)
throwToolExit('replay-from location not specified');
await enableReplayProcessManager(replayFrom);
enableReplayFileSystem(replayFrom);
await enableReplayPlatform(replayFrom);
contextOverrides.addAll(<Type, dynamic>{
ProcessManager: await getReplayProcessManager(replayFrom),
FileSystem: getReplayFileSystem(replayFrom),
Platform: await getReplayPlatform(replayFrom),
});
VMService.enableReplayConnection(replayFrom);
}
logger.quiet = topLevelResults['quiet'];
if (topLevelResults.wasParsed('color'))
logger.supportsColor = topLevelResults['color'];
// We must set Cache.flutterRoot early because other features use it (e.g.
// enginePath's initializer uses it).
final String flutterRoot = topLevelResults['flutter-root'] ?? _defaultFlutterRoot;
Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
await Cache.lock();
// Set up the tooling configuration.
final String enginePath = _findEnginePath(topLevelResults);
if (enginePath != null) {
contextOverrides.addAll(<Type, dynamic>{
Artifacts: Artifacts.getLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath)),
});
}
if (topLevelResults['suppress-analytics'])
flutterUsage.suppressAnalytics = true;
await context.run<Future<Null>>(
overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) {
return new MapEntry<Type, Generator>(type, () => value);
}),
body: () async {
logger.quiet = topLevelResults['quiet'];
_checkFlutterCopy();
await FlutterVersion.instance.ensureVersionFile();
if (topLevelResults.command?.name != 'upgrade') {
await FlutterVersion.instance.checkFlutterVersionFreshness();
}
if (topLevelResults.wasParsed('color'))
logger.supportsColor = topLevelResults['color'];
if (topLevelResults.wasParsed('packages'))
PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages']));
// We must set Cache.flutterRoot early because other features use it (e.g.
// enginePath's initializer uses it).
final String flutterRoot = topLevelResults['flutter-root'] ?? _defaultFlutterRoot;
Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
// See if the user specified a specific device.
deviceManager.specifiedDeviceId = topLevelResults['device-id'];
if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
await Cache.lock();
// Set up the tooling configuration.
final String enginePath = _findEnginePath(topLevelResults);
if (enginePath != null) {
Artifacts.useLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath));
}
if (topLevelResults['suppress-analytics'])
flutterUsage.suppressAnalytics = true;
// The Android SDK could already have been set by tests.
context.putIfAbsent(AndroidSdk, AndroidSdk.locateAndroidSdk);
_checkFlutterCopy();
await FlutterVersion.instance.ensureVersionFile();
if (topLevelResults.command?.name != 'upgrade') {
await FlutterVersion.instance.checkFlutterVersionFreshness();
}
if (topLevelResults['version']) {
flutterUsage.sendCommand('version');
String status;
if (topLevelResults['machine']) {
status = const JsonEncoder.withIndent(' ').convert(FlutterVersion.instance.toJson());
} else {
status = FlutterVersion.instance.toString();
}
printStatus(status);
return;
}
if (topLevelResults.wasParsed('packages'))
PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages']));
// See if the user specified a specific device.
deviceManager.specifiedDeviceId = topLevelResults['device-id'];
if (topLevelResults['version']) {
flutterUsage.sendCommand('version');
String status;
if (topLevelResults['machine']) {
status = const JsonEncoder.withIndent(' ').convert(FlutterVersion.instance.toJson());
} else {
status = FlutterVersion.instance.toString();
}
printStatus(status);
return;
}
if (topLevelResults['machine']) {
throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
}
if (topLevelResults['machine']) {
throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
}
await super.runCommand(topLevelResults);
await super.runCommand(topLevelResults);
},
);
}
String _tryEnginePath(String enginePath) {
......
......@@ -49,7 +49,7 @@ class Usage {
}
/// Returns [Usage] active in the current app context.
static Usage get instance => context.putIfAbsent(Usage, () => new Usage());
static Usage get instance => context[Usage];
Analytics _analytics;
......
......@@ -167,7 +167,7 @@ class FlutterVersion {
await _run(<String>['git', 'remote', 'remove', _kVersionCheckRemote]);
}
static FlutterVersion get instance => context.putIfAbsent(FlutterVersion, () => new FlutterVersion(const Clock()));
static FlutterVersion get instance => context[FlutterVersion];
/// Return a short string for the version (e.g. `master/0.0.59-pre.92`, `scroll_refactor/a76bc8e22b`).
String getVersionString({bool redactUnknownBranches: false}) {
......
......@@ -432,26 +432,30 @@ void main() {
snapshotPath: 'output.snapshot',
depfileContent: 'output.snapshot : main.dart other.dart',
);
context.setVariable(GenSnapshot, genSnapshot);
await fs.file('main.dart').writeAsString('import "other.dart";\nvoid main() {}');
await fs.file('other.dart').writeAsString('import "main.dart";\nvoid main() {}');
await fs.file('output.snapshot').create();
await fs.file('output.snapshot.d').writeAsString('output.snapshot : main.dart');
await writeFingerprint(files: <String, String>{
'main.dart': 'bc096b33f14dde5e0ffaf93a1d03395c',
'other.dart': 'e0c35f083f0ad76b2d87100ec678b516',
'output.snapshot': 'd41d8cd98f00b204e9800998ecf8427e',
});
await buildSnapshot(mainPath: 'other.dart');
expect(genSnapshot.callCount, 1);
expectFingerprintHas(
entryPoint: 'other.dart',
checksums: <String, String>{
'main.dart': 'bc096b33f14dde5e0ffaf93a1d03395c',
'other.dart': 'e0c35f083f0ad76b2d87100ec678b516',
'output.snapshot': 'd41d8cd98f00b204e9800998ecf8427e',
await context.run(
overrides: <Type, Generator>{GenSnapshot: () => genSnapshot},
body: () async {
await fs.file('main.dart').writeAsString('import "other.dart";\nvoid main() {}');
await fs.file('other.dart').writeAsString('import "main.dart";\nvoid main() {}');
await fs.file('output.snapshot').create();
await fs.file('output.snapshot.d').writeAsString('output.snapshot : main.dart');
await writeFingerprint(files: <String, String>{
'main.dart': 'bc096b33f14dde5e0ffaf93a1d03395c',
'other.dart': 'e0c35f083f0ad76b2d87100ec678b516',
'output.snapshot': 'd41d8cd98f00b204e9800998ecf8427e',
});
await buildSnapshot(mainPath: 'other.dart');
expect(genSnapshot.callCount, 1);
expectFingerprintHas(
entryPoint: 'other.dart',
checksums: <String, String>{
'main.dart': 'bc096b33f14dde5e0ffaf93a1d03395c',
'other.dart': 'e0c35f083f0ad76b2d87100ec678b516',
'output.snapshot': 'd41d8cd98f00b204e9800998ecf8427e',
},
);
},
);
}, overrides: contextOverrides);
......
......@@ -32,11 +32,6 @@ void main() {
});
group('flags', () {
test('returns no-op flags when not inside Flutter runner', () {
expect(flags, isNotNull);
expect(flags['foo'], isNull);
});
testUsingContext('returns null for undefined flags', () async {
await runCommand(<String>[], () {
expect(flags['undefined-flag'], isNull);
......
......@@ -5,7 +5,6 @@
import 'dart:async';
import 'package:flutter_tools/src/android/android_workflow.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:flutter_tools/src/globals.dart';
......@@ -18,14 +17,11 @@ import '../src/mocks.dart';
void main() {
Daemon daemon;
AppContext appContext;
NotifyingLogger notifyingLogger;
group('daemon', () {
setUp(() {
appContext = new AppContext();
notifyingLogger = new NotifyingLogger();
appContext.setVariable(Logger, notifyingLogger);
});
tearDown(() {
......@@ -51,51 +47,51 @@ void main() {
commands.close();
});
testUsingContext('printError should send daemon.logMessage event', () {
return appContext.runInZone(() async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger
);
printError('daemon.logMessage test');
final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'error';
});
expect(response['id'], isNull);
expect(response['event'], 'daemon.logMessage');
final Map<String, String> logMessage = response['params'];
expect(logMessage['level'], 'error');
expect(logMessage['message'], 'daemon.logMessage test');
responses.close();
commands.close();
testUsingContext('printError should send daemon.logMessage event', () async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger,
);
printError('daemon.logMessage test');
final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'error';
});
expect(response['id'], isNull);
expect(response['event'], 'daemon.logMessage');
final Map<String, String> logMessage = response['params'];
expect(logMessage['level'], 'error');
expect(logMessage['message'], 'daemon.logMessage test');
responses.close();
commands.close();
}, overrides: <Type, Generator>{
Logger: () => notifyingLogger,
});
testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
final StringBuffer buffer = new StringBuffer();
await runZoned(() async {
return appContext.runInZone(() async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger,
logToStdout: true
);
printStatus('daemon.logMessage test');
// Service the event loop.
await new Future<Null>.value();
});
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger,
logToStdout: true,
);
printStatus('daemon.logMessage test');
// Service the event loop.
await new Future<Null>.value();
}, zoneSpecification: new ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
buffer.writeln(line);
}));
expect(buffer.toString().trim(), 'daemon.logMessage test');
}, overrides: <Type, Generator>{
Logger: () => notifyingLogger,
});
testUsingContext('daemon.shutdown command should stop daemon', () async {
......
......@@ -36,7 +36,7 @@ void main() {
tearDown(() {
tools.crashFileSystem = const LocalFileSystem();
tools.writelnStderr = stderr.writeln;
tools.writelnStderr = const Stdio().stderr.writeln;
restoreExitFunction();
});
......
......@@ -28,7 +28,7 @@ void main() {
testUsingContext('pub get 69', () async {
String error;
final MockProcessManager processMock = context.getVariable(ProcessManager);
final MockProcessManager processMock = context[ProcessManager];
new FakeAsync().run((FakeAsync time) {
expect(processMock.lastPubEnvironment, isNull);
......@@ -96,8 +96,8 @@ void main() {
testUsingContext('pub cache in root is used', () async {
String error;
final MockProcessManager processMock = context.getVariable(ProcessManager);
final MockFileSystem fsMock = context.getVariable(FileSystem);
final MockProcessManager processMock = context[ProcessManager];
final MockFileSystem fsMock = context[FileSystem];
new FakeAsync().run((FakeAsync time) {
MockDirectory.findCache = true;
......@@ -123,7 +123,7 @@ void main() {
testUsingContext('pub cache in environment is used', () async {
String error;
final MockProcessManager processMock = context.getVariable(ProcessManager);
final MockProcessManager processMock = context[ProcessManager];
new FakeAsync().run((FakeAsync time) {
MockDirectory.findCache = true;
......
......@@ -27,7 +27,7 @@ void main() {
Directory tempDir;
String basePath;
DevFS devFS;
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final AssetBundle assetBundle = AssetBundleFactory.defaultInstance.createBundle();
setUpAll(() {
fs = new MemoryFileSystem();
......
// Copyright 2015 The Chromium 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 'package:file/local.dart';
import 'package:flutter_tools/runner.dart' as tools;
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/io.dart' as io;
import 'package:flutter_tools/src/base/port_scanner.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:test/test.dart';
import '../src/common.dart';
import '../src/context.dart';
/// Runs the specified [testMethod] in a minimal `AppContext` that is set up
/// to redirect log output to a `BufferLogger` to avoid spamming `stdout`.
///
/// Test methods will generally want to use [expectProcessExits] in their method
/// bodies.
void testReplay(
String description,
dynamic testMethod(), {
Timeout timeout,
Map<Type, Generator> overrides: const <Type, Generator>{},
bool skip,
}) {
setUp(() {
io.setExitFunctionForTests();
});
tearDown(() {
io.restoreExitFunction();
});
testUsingContext(
description,
testMethod,
timeout: timeout,
overrides: overrides,
skip: skip,
initializeContext: (AppContext testContext) {
testContext.putIfAbsent(PortScanner, () => new MockPortScanner());
},
);
}
/// Expects that the specified [command] to Flutter tools exits with the
/// specified [exitCode] (defaults to zero). It is expected that callers will
/// be running in a test via [testReplay].
///
/// [command] should be the list of arguments that are passed to the `flutter`
/// command-line tool. For example:
///
/// ```
/// <String>[
/// 'run',
/// '--no-hot',
/// '--no-resident',
/// ]
/// ```
void expectProcessExits(
FlutterCommand command, {
List<String> args: const <String>[],
dynamic exitCode: 0,
}) {
final Future<Null> runFuture = tools.run(
<String>[command.name]..addAll(args),
<FlutterCommand>[command],
reportCrashes: false,
flutterVersion: 'test',
);
expect(runFuture, throwsProcessExit(exitCode));
}
/// The base path of the replay tests.
String get replayBase {
return const LocalFileSystem().path.joinAll(<String>[
Cache.flutterRoot,
'packages',
'flutter_tools',
'test',
'replay',
]);
}
......@@ -43,39 +43,41 @@ class TestRunner extends ResidentRunner {
}
void main() {
TestRunner testRunner;
setUp(() {
TestRunner createTestRunner() {
// TODO(jacobr): make these tests run with `previewDart2: true` and
// `trackWidgetCreation: true` as well as the default flags.
// Currently the TestRunner is not properly configured to be able to run
// with `previewDart2: true` due to missing resources.
testRunner = new TestRunner(
return new TestRunner(
<FlutterDevice>[new FlutterDevice(
new MockDevice(),
previewDart2: false,
trackWidgetCreation: false,
)],
);
});
}
group('keyboard input handling', () {
testUsingContext('single help character', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.hasHelpBeenPrinted, isFalse);
await testRunner.processTerminalInput('h');
expect(testRunner.hasHelpBeenPrinted, isTrue);
});
testUsingContext('help character surrounded with newlines', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.hasHelpBeenPrinted, isFalse);
await testRunner.processTerminalInput('\nh\n');
expect(testRunner.hasHelpBeenPrinted, isTrue);
});
testUsingContext('reload character with trailing newline', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.receivedCommand, isNull);
await testRunner.processTerminalInput('r\n');
expect(testRunner.receivedCommand, equals('r'));
});
testUsingContext('newlines', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.receivedCommand, isNull);
await testRunner.processTerminalInput('\n\n');
expect(testRunner.receivedCommand, equals(''));
......
......@@ -3,77 +3,46 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
import 'package:flutter_tools/src/android/android_workflow.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/port_scanner.dart';
import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/context_runner.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/usage.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:quiver/time.dart';
import 'package:test/test.dart';
import 'common.dart';
export 'package:flutter_tools/src/base/context.dart' show Generator;
/// Return the test logger. This assumes that the current Logger is a BufferLogger.
BufferLogger get testLogger => context[Logger];
MockDeviceManager get testDeviceManager => context[DeviceManager];
MockDoctor get testDoctor => context[Doctor];
typedef dynamic Generator();
typedef void ContextInitializer(AppContext testContext);
void _defaultInitializeContext(AppContext testContext) {
testContext
..putIfAbsent(DeviceManager, () => new MockDeviceManager())
..putIfAbsent(DevFSConfig, () => new DevFSConfig())
..putIfAbsent(Doctor, () => new MockDoctor())
..putIfAbsent(HotRunnerConfig, () => new HotRunnerConfig())
..putIfAbsent(Cache, () => new Cache())
..putIfAbsent(Artifacts, () => new CachedArtifacts())
..putIfAbsent(OperatingSystemUtils, () => new MockOperatingSystemUtils())
..putIfAbsent(PortScanner, () => new MockPortScanner())
..putIfAbsent(Xcode, () => new Xcode())
..putIfAbsent(XcodeProjectInterpreter, () => new MockXcodeProjectInterpreter())
..putIfAbsent(IOSSimulatorUtils, () {
final MockIOSSimulatorUtils mock = new MockIOSSimulatorUtils();
when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]);
return mock;
})
..putIfAbsent(SimControl, () => new MockSimControl())
..putIfAbsent(Usage, () => new MockUsage())
..putIfAbsent(FlutterVersion, () => new MockFlutterVersion())
..putIfAbsent(Clock, () => const Clock())
..putIfAbsent(HttpClient, () => new MockHttpClient());
}
void testUsingContext(String description, dynamic testMethod(), {
Timeout timeout,
Map<Type, Generator> overrides: const <Type, Generator>{},
ContextInitializer initializeContext: _defaultInitializeContext,
String testOn,
bool skip, // should default to `false`, but https://github.com/dart-lang/test/issues/545 doesn't allow this
}) {
// Ensure we don't rely on the default [Config] constructor which will
// leak a sticky $HOME/.flutter_settings behind!
Directory configDir;
......@@ -89,45 +58,58 @@ void testUsingContext(String description, dynamic testMethod(), {
}
test(description, () async {
final AppContext testContext = new AppContext();
// The context always starts with these value since others depend on them.
testContext
..putIfAbsent(BotDetector, () => const BotDetector())
..putIfAbsent(Stdio, () => const Stdio())
..putIfAbsent(Platform, () => const LocalPlatform())
..putIfAbsent(FileSystem, () => const LocalFileSystem())
..putIfAbsent(ProcessManager, () => const LocalProcessManager())
..putIfAbsent(Logger, () => new BufferLogger())
..putIfAbsent(Config, () => buildConfig(testContext[FileSystem]));
// Apply the initializer after seeding the base value above.
initializeContext(testContext);
final String flutterRoot = getFlutterRoot();
try {
return await testContext.runInZone(() async {
// Apply the overrides to the test context in the zone since their
// instantiation may reference items already stored on the context.
overrides.forEach((Type type, dynamic value()) {
context.setVariable(type, value());
});
// Provide a sane default for the flutterRoot directory. Individual
// tests can override this either in the test or during setup.
Cache.flutterRoot ??= flutterRoot;
return await testMethod();
}, onError: (dynamic error, StackTrace stackTrace) {
_printBufferedErrors(testContext);
throw error;
});
} catch (error) {
_printBufferedErrors(testContext);
rethrow;
}
await runInContext<dynamic>(() {
return context.run<Future<dynamic>>(
name: 'mocks',
overrides: <Type, Generator>{
Config: () => buildConfig(fs),
DeviceManager: () => new MockDeviceManager(),
Doctor: () => new MockDoctor(),
FlutterVersion: () => new MockFlutterVersion(),
HttpClient: () => new MockHttpClient(),
IOSSimulatorUtils: () {
final MockIOSSimulatorUtils mock = new MockIOSSimulatorUtils();
when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]);
return mock;
},
Logger: () => new BufferLogger(),
OperatingSystemUtils: () => new MockOperatingSystemUtils(),
PortScanner: () => new MockPortScanner(),
SimControl: () => new MockSimControl(),
Usage: () => new MockUsage(),
XcodeProjectInterpreter: () => new MockXcodeProjectInterpreter(),
},
body: () {
final String flutterRoot = getFlutterRoot();
return runZoned(() {
try {
return context.run<Future<dynamic>>(
// Apply the overrides to the test context in the zone since their
// instantiation may reference items already stored on the context.
overrides: overrides,
name: 'test-specific overrides',
body: () async {
// Provide a sane default for the flutterRoot directory. Individual
// tests can override this either in the test or during setup.
Cache.flutterRoot ??= flutterRoot;
return await testMethod();
},
);
} catch (error) {
_printBufferedErrors(context);
rethrow;
}
}, onError: (dynamic error, StackTrace stackTrace) {
io.stdout.writeln(error);
io.stdout.writeln(stackTrace);
_printBufferedErrors(context);
throw error;
});
},
);
});
}, timeout: timeout, testOn: testOn, skip: skip);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment