// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// @dart = 2.8

import 'dart:async';

import 'package:dds/dds.dart' as dds;
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/command_help.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart' as io;
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_cold.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;

import '../src/common.dart';
import '../src/context.dart';
import '../src/fake_vm_services.dart';
import '../src/fakes.dart';
import '../src/testbed.dart';

final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kResume,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[],
  exceptionPauseMode: null,
  extensionRPCs: <String>[],
  libraries: <vm_service.LibraryRef>[
    vm_service.LibraryRef(
      id: '1',
      uri: 'file:///hello_world/main.dart',
      name: '',
    ),
  ],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
  isSystemIsolate: false,
  isolateFlags: <vm_service.IsolateFlag>[],
);

final vm_service.Isolate fakePausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kPauseException,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[
    vm_service.Breakpoint(
      breakpointNumber: 123,
      id: 'test-breakpoint',
      location: vm_service.SourceLocation(
        tokenPos: 0,
        script: vm_service.ScriptRef(id: 'test-script', uri: 'foo.dart'),
      ),
      enabled: true,
      resolved: true,
    ),
  ],
  exceptionPauseMode: null,
  libraries: <vm_service.LibraryRef>[],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
  isSystemIsolate: false,
  isolateFlags: <vm_service.IsolateFlag>[],
);

final vm_service.VM fakeVM = vm_service.VM(
  isolates: <vm_service.IsolateRef>[fakeUnpausedIsolate],
  pid: 1,
  hostCPU: '',
  isolateGroups: <vm_service.IsolateGroupRef>[],
  targetCPU: '',
  startTime: 0,
  name: 'dart',
  architectureBits: 64,
  operatingSystem: '',
  version: '',
  systemIsolateGroups: <vm_service.IsolateGroupRef>[],
  systemIsolates: <vm_service.IsolateRef>[],
);

final FlutterView fakeFlutterView = FlutterView(
  id: 'a',
  uiIsolate: fakeUnpausedIsolate,
);

final FakeVmServiceRequest listViews = FakeVmServiceRequest(
  method: kListViewsMethod,
  jsonResponse: <String, Object>{
    'views': <Object>[
      fakeFlutterView.toJson(),
    ],
  },
);

const FakeVmServiceRequest setAssetBundlePath = FakeVmServiceRequest(
  method: '_flutter.setAssetBundlePath',
  args: <String, Object>{
    'viewId': 'a',
    'assetDirectory': 'build/flutter_assets',
    'isolateId': '1',
  }
);

const FakeVmServiceRequest evict = FakeVmServiceRequest(
  method: 'ext.flutter.evict',
  args: <String, Object>{
    'value': 'asset',
    'isolateId': '1',
  }
);

final Uri testUri = Uri.parse('foo://bar');

void main() {
  Testbed testbed;
  FakeFlutterDevice flutterDevice;
  FakeDevFS devFS;
  ResidentRunner residentRunner;
  FakeDevice device;
  FakeVmServiceHost fakeVmServiceHost;

  setUp(() {
    testbed = Testbed(setup: () {
      globals.fs.file('.packages')
        .writeAsStringSync('\n');
      globals.fs.file(globals.fs.path.join('build', 'app.dill'))
        ..createSync(recursive: true)
        ..writeAsStringSync('ABC');
      residentRunner = HotRunner(
        <FlutterDevice>[
          flutterDevice,
        ],
        stayResident: false,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
        target: 'main.dart',
        devtoolsHandler: createNoOpHandler,
      );
    });
    device = FakeDevice();
    devFS = FakeDevFS();
    flutterDevice = FakeFlutterDevice()
      ..testUri = testUri
      ..vmServiceHost = (() => fakeVmServiceHost)
      ..device = device
      .._devFS = devFS;
  });

  testUsingContext('ResidentRunner can attach to device successfully', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    final Future<int> result = residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    );
    final Future<DebugConnectionInfo> connectionInfo = futureConnectionInfo.future;

    expect(await result, 0);
    expect(futureConnectionInfo.isCompleted, true);
    expect((await connectionInfo).baseUri, 'foo://bar');
    expect(futureAppStart.isCompleted, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner suppresses errors for the initial compilation', () => testbed.run(() async {
    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
      ..nextOutput = const CompilerOutput('foo', 0 ,<Uri>[]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    flutterDevice.generator = residentCompiler;

    expect(await residentRunner.run(enableDevTools: true), 0);
    expect(residentCompiler.didSuppressErrors, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  // Regression test for https://github.com/flutter/flutter/issues/60613
  testUsingContext('ResidentRunner calls appFailedToStart if initial compilation fails', () => testbed.run(() async {
    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
      ..nextOutput = const CompilerOutput('foo', 1 ,<Uri>[]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    flutterDevice.generator = residentCompiler;

    expect(await residentRunner.run(), 1);
    // Completing this future ensures that the daemon can exit correctly.
    expect(await residentRunner.waitForAppToFinish(), 1);
  }));

  // Regression test for https://github.com/flutter/flutter/issues/60613
  testUsingContext('ResidentRunner calls appFailedToStart if initial compilation fails - cold mode', () => testbed.run(() async {
    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    residentRunner = ColdRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.release),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    flutterDevice.runColdCode = 1;

    expect(await residentRunner.run(), 1);
    // Completing this future ensures that the daemon can exit correctly.
    expect(await residentRunner.waitForAppToFinish(), 1);
  }));

  // Regression test for https://github.com/flutter/flutter/issues/60613
  testUsingContext('ResidentRunner calls appFailedToStart if exception is thrown - cold mode', () => testbed.run(() async {
    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    residentRunner = ColdRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.release),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    flutterDevice.runColdError = Exception('BAD STUFF');


    expect(await residentRunner.run(), 1);
    // Completing this future ensures that the daemon can exit correctly.
    expect(await residentRunner.waitForAppToFinish(), 1);
  }));

  testUsingContext('ResidentRunner does not suppressErrors if running with an applicationBinary', () => testbed.run(() async {
    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
      ..nextOutput = const CompilerOutput('foo', 0 ,<Uri>[]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      applicationBinary: globals.fs.file('app.apk'),
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    flutterDevice.generator = residentCompiler;

    expect(await residentRunner.run(enableDevTools: true), 0);
    expect(residentCompiler.didSuppressErrors, false);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner can attach to device successfully with --fast-start', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        }
      ),
      FakeVmServiceRequest(
        method: kRunInViewMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
          'mainScript': 'main.dart.dill',
          'assetDirectory': 'build/flutter_assets',
        }
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Isolate',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventKind.kIsolateRunnable,
        )
      ),
    ]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(
        BuildInfo.debug,
        fastStart: true,
        startPaused: true,
      ),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    final Future<int> result = residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    );
    final Future<DebugConnectionInfo> connectionInfo = futureConnectionInfo.future;

    expect(await result, 0);
    expect(futureConnectionInfo.isCompleted, true);
    expect((await connectionInfo).baseUri, 'foo://bar');
    expect(futureAppStart.isCompleted, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.reportError = vm_service.RPCError('something bad happened', 666, '');

    final OperationResult result = await residentRunner.restart();
    expect(result.fatal, true);
    expect(result.code, 1);
    expect((globals.flutterUsage as TestUsage).events, contains(
      TestUsageEvent('hot', 'exception', parameters: CustomDimensions(
        hotEventTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
        hotEventSdkName: 'Android',
        hotEventEmulator: false,
        hotEventFullRestart: false,
        fastReassemble: false,
      )),
    ));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    Usage: () => TestUsage(),
  }));

  testUsingContext('ResidentRunner fails its operation if the device initialization is not complete', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
    ));
    await futureAppStart.future;
    flutterDevice._devFS = null;

    final OperationResult result = await residentRunner.restart();
    expect(result.fatal, false);
    expect(result.code, 1);
    expect(result.message, contains('Device initialization has not completed.'));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner can handle an reload-barred exception from hot reload', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.reportError = vm_service.RPCError('something bad happened', kIsolateReloadBarred, '');

    final OperationResult result = await residentRunner.restart();
    expect(result.fatal, true);
    expect(result.code, kIsolateReloadBarred);
    expect(result.message, contains('Unable to hot reload application due to an unrecoverable error'));

    expect((globals.flutterUsage as TestUsage).events, contains(
      TestUsageEvent('hot', 'reload-barred', parameters: CustomDimensions(
        hotEventTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
        hotEventSdkName: 'Android',
        hotEventEmulator: false,
        hotEventFullRestart: false,
        fastReassemble: false,
      )),
    ));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    Usage: () => TestUsage(),
  }));

  testUsingContext('ResidentRunner reports hot reload event with null safety analytics', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
    ]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      target: 'main.dart',
      debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
        BuildMode.debug, '', treeShakeIcons: false, extraFrontEndOptions: <String>[
        '--enable-experiment=non-nullable',
        ],
      )),
      devtoolsHandler: createNoOpHandler,
    );
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.reportError = vm_service.RPCError('something bad happened', 666, '');

    final OperationResult result = await residentRunner.restart();
    expect(result.fatal, true);
    expect(result.code, 1);

    expect((globals.flutterUsage as TestUsage).events, contains(
      TestUsageEvent('hot', 'exception', parameters: CustomDimensions(
        hotEventTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
        hotEventSdkName: 'Android',
        hotEventEmulator: false,
        hotEventFullRestart: false,
        fastReassemble: false,
      )),
    ));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    Usage: () => TestUsage(),
  }));

  testUsingContext('ResidentRunner does not reload sources if no sources changed', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
     listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': '1',
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'ext.flutter.reassemble',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
      ),
    ]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.report =  UpdateFSReport(success: true);

    final OperationResult result = await residentRunner.restart();

    expect(result.code, 0);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner reports error with missing entrypoint file', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{
          'isolates': <Object>[
            fakeUnpausedIsolate.toJson(),
          ],
        }).toJson(),
      ),
      const FakeVmServiceRequest(
        method: 'reloadSources',
        args: <String, Object>{
          'isolateId': '1',
          'pause': false,
          'rootLibUri': 'main.dart.incremental.dill'
        },
        jsonResponse: <String, Object>{
          'type': 'ReloadReport',
          'success': true,
          'details': <String, Object>{
            'loadedLibraryCount': 1,
          },
        },
      ),
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': '1',
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'ext.flutter.reassemble',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
      ),
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.report =  UpdateFSReport(success: true, invalidatedSourcesCount: 1);

    final OperationResult result = await residentRunner.restart();

    expect(globals.fs.file(globals.fs.path.join('lib', 'main.dart')), isNot(exists));
    expect(testLogger.errorText, contains('The entrypoint file (i.e. the file with main())'));
    expect(result.fatal, false);
    expect(result.code, 0);
  }));

   testUsingContext('ResidentRunner resets compilation time on reload reject', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{
          'isolates': <Object>[
            fakeUnpausedIsolate.toJson(),
          ],
        }).toJson(),
      ),
      const FakeVmServiceRequest(
        method: 'reloadSources',
        args: <String, Object>{
          'isolateId': '1',
          'pause': false,
          'rootLibUri': 'main.dart.incremental.dill'
        },
        jsonResponse: <String, Object>{
          'type': 'ReloadReport',
          'success': false,
          'notices': <Object>[
            <String, Object>{
              'message': 'Failed to hot reload'
            }
          ],
          'details': <String, Object>{},
        },
      ),
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': '1',
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'ext.flutter.reassemble',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
      ),
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.report =  UpdateFSReport(success: true, invalidatedSourcesCount: 1);

    final OperationResult result = await residentRunner.restart();

    expect(result.fatal, false);
    expect(result.message, contains('Reload rejected: Failed to hot reload')); // contains error message from reload report.
    expect(result.code, 1);
    expect(devFS.lastCompiled, null);
  }));

  testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{
          'isolates': <Object>[
            fakeUnpausedIsolate.toJson(),
          ],
        }).toJson(),
      ),
      const FakeVmServiceRequest(
        method: 'reloadSources',
        args: <String, Object>{
          'isolateId': '1',
          'pause': false,
          'rootLibUri': 'main.dart.incremental.dill'
        },
        jsonResponse: <String, Object>{
          'type': 'ReloadReport',
          'success': true,
          'details': <String, Object>{
            'loadedLibraryCount': 1,
          },
        },
      ),
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': '1',
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'ext.flutter.reassemble',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
      ),
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;

    final OperationResult result = await residentRunner.restart();
    expect(result.fatal, false);
    expect(result.code, 0);

    final TestUsageEvent event = (globals.flutterUsage as TestUsage).events.first;
    expect(event.category, 'hot');
    expect(event.parameter, 'reload');
    expect(event.parameters.hotEventTargetPlatform, getNameForTargetPlatform(TargetPlatform.android_arm));
  }, overrides: <Type, Generator>{
    Usage: () => TestUsage(),
  }));

  testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: fakeVM.toJson(),
      ),
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: fakeVM.toJson(),
      ),
      const FakeVmServiceRequest(
        method: 'reloadSources',
        args: <String, Object>{
          'isolateId': '1',
          'pause': false,
          'rootLibUri': 'main.dart.incremental.dill',
        },
        jsonResponse: <String, Object>{
          'type': 'ReloadReport',
          'success': true,
          'details': <String, Object>{
            'loadedLibraryCount': 1,
          },
        },
      ),
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': '1',
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'ext.flutter.fastReassemble',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
          'className': 'FOO',
        },
      ),
    ]);
    final FakeDelegateFlutterDevice flutterDevice = FakeDelegateFlutterDevice(
      device,
      BuildInfo.debug,
      FakeResidentCompiler(),
      devFS,
    )..vmService = fakeVmServiceHost.vmService;
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    devFS.nextUpdateReport = UpdateFSReport(
      success: true,
      fastReassembleClassName: 'FOO',
      invalidatedSourcesCount: 1,
    );

    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));

    await futureAppStart.future;
    final OperationResult result = await residentRunner.restart();

    expect(result.fatal, false);
    expect(result.code, 0);

    final TestUsageEvent event = (globals.flutterUsage as TestUsage).events.first;
    expect(event.category, 'hot');
    expect(event.parameter, 'reload');
    expect(event.parameters.fastReassemble, true);
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem.test(),
    Platform: () => FakePlatform(),
    ProjectFileInvalidator: () => FakeProjectFileInvalidator(),
    Usage: () => TestUsage(),
    FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true),
  }));

  testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      FakeVmServiceRequest(
        method: kRunInViewMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
          'mainScript': 'main.dart.dill',
          'assetDirectory': 'build/flutter_assets',
        },
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Isolate',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventKind.kIsolateRunnable,
        )
      )
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));

    final OperationResult result = await residentRunner.restart(fullRestart: true);
    expect(result.fatal, false);
    expect(result.code, 0);

    final TestUsageEvent event = (globals.flutterUsage as TestUsage).events.first;
    expect(event.category, 'hot');
    expect(event.parameter, 'restart');
    expect(event.parameters.hotEventTargetPlatform, getNameForTargetPlatform(TargetPlatform.android_arm));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    Usage: () => TestUsage(),
  }));

  testUsingContext('ResidentRunner can remove breakpoints and exception-pause-mode from paused isolate during hot restart', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
        jsonResponse: fakePausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
      ),
      const FakeVmServiceRequest(
        method: 'setIsolatePauseMode',
        args: <String, String>{
          'isolateId': '1',
          'exceptionPauseMode': 'None',
        }
      ),
      const FakeVmServiceRequest(
        method: 'removeBreakpoint',
        args: <String, String>{
          'isolateId': '1',
          'breakpointId': 'test-breakpoint',
        }
      ),
      const FakeVmServiceRequest(
        method: 'resume',
        args: <String, String>{
          'isolateId': '1',
        }
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      FakeVmServiceRequest(
        method: kRunInViewMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
          'mainScript': 'main.dart.dill',
          'assetDirectory': 'build/flutter_assets',
        },
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Isolate',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventKind.kIsolateRunnable,
        )
      )
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));

    final OperationResult result = await residentRunner.restart(fullRestart: true);

    expect(result.isOk, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner will alternative the name of the dill file uploaded for a hot restart', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      FakeVmServiceRequest(
        method: kRunInViewMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
          'mainScript': 'main.dart.dill',
          'assetDirectory': 'build/flutter_assets',
        },
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Isolate',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventKind.kIsolateRunnable,
        ),
      ),
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      FakeVmServiceRequest(
        method: kRunInViewMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
          'mainScript': 'main.dart.swap.dill',
          'assetDirectory': 'build/flutter_assets',
        },
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Isolate',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventKind.kIsolateRunnable,
        ),
      ),
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        args: <String, Object>{
          'isolateId': fakeUnpausedIsolate.id,
        },
        jsonResponse: fakeUnpausedIsolate.toJson(),
      ),
      FakeVmServiceRequest(
        method: 'getVM',
        jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      FakeVmServiceRequest(
        method: kRunInViewMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
          'mainScript': 'main.dart.dill',
          'assetDirectory': 'build/flutter_assets',
        },
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Isolate',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventKind.kIsolateRunnable,
        ),
      )
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));

    await residentRunner.restart(fullRestart: true);
    await residentRunner.restart(fullRestart: true);
    await residentRunner.restart(fullRestart: true);

    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner Can handle an RPC exception from hot restart', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
    final Completer<void> futureAppStart = Completer<void>.sync();
    unawaited(residentRunner.attach(
      appStartedCompleter: futureAppStart,
      connectionInfoCompleter: futureConnectionInfo,
      enableDevTools: true,
    ));
    await futureAppStart.future;
    flutterDevice.reportError = vm_service.RPCError('something bad happened', 666, '');

    final OperationResult result = await residentRunner.restart(fullRestart: true);
    expect(result.fatal, true);
    expect(result.code, 1);

    expect((globals.flutterUsage as TestUsage).events, contains(
      TestUsageEvent('hot', 'exception', parameters: CustomDimensions(
        hotEventTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
        hotEventSdkName: 'Android',
        hotEventEmulator: false,
        hotEventFullRestart: true,
        fastReassemble: false,
      )),
    ));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    Usage: () => TestUsage(),
  }));

  testUsingContext('ResidentRunner uses temp directory when there is no output dill path', () => testbed.run(() {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    expect(residentRunner.artifactDirectory.path, contains('flutter_tool.'));

    final ResidentRunner otherRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      dillOutputPath: globals.fs.path.join('foobar', 'app.dill'),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    expect(otherRunner.artifactDirectory.path, contains('foobar'));
  }));

  testUsingContext('ResidentRunner deletes artifact directory on preExit', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    residentRunner.artifactDirectory.childFile('app.dill').createSync();
    await residentRunner.preExit();

    expect(residentRunner.artifactDirectory, isNot(exists));
  }));

  testUsingContext('ResidentRunner can run source generation', () => testbed.run(() async {
    final File arbFile = globals.fs.file(globals.fs.path.join('lib', 'l10n', 'app_en.arb'))
      ..createSync(recursive: true);
    arbFile.writeAsStringSync('''
{
  "helloWorld": "Hello, World!",
  "@helloWorld": {
    "description": "Sample description"
  }
}''');
    globals.fs.file('l10n.yaml').createSync();
    globals.fs.file('pubspec.yaml').writeAsStringSync('flutter:\n  generate: true\n');

    // Create necessary files for [DartPluginRegistrantTarget]
    final File packageConfig = globals.fs.directory('.dart_tool')
        .childFile('package_config.json');
    packageConfig.createSync(recursive: true);
    packageConfig.writeAsStringSync('''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "path_provider_linux",
      "rootUri": "../../../path_provider_linux",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ]
}
''');
    // Start from an empty dart_plugin_registrant.dart file.
    globals.fs.directory('.dart_tool').childDirectory('flutter_build').childFile('dart_plugin_registrant.dart').createSync(recursive: true);

    await residentRunner.runSourceGenerators();

    expect(testLogger.errorText, isEmpty);
    expect(testLogger.statusText, isEmpty);
  }));

  testUsingContext('generated main uses correct target', () => testbed.run(() async {
    final File arbFile = globals.fs.file(globals.fs.path.join('lib', 'l10n', 'app_en.arb'))
      ..createSync(recursive: true);
    arbFile.writeAsStringSync('''
{
  "helloWorld": "Hello, World!",
  "@helloWorld": {
    "description": "Sample description"
  }
}''');
    globals.fs.file('l10n.yaml').createSync();
    globals.fs.file('pubspec.yaml').writeAsStringSync('''
flutter:
  generate: true

dependencies:
  flutter:
    sdk: flutter
  path_provider_linux: 1.0.0
''');

    // Create necessary files for [DartPluginRegistrantTarget], including a
    // plugin that will trigger generation.
    final File packageConfig = globals.fs.directory('.dart_tool')
        .childFile('package_config.json');
    packageConfig.createSync(recursive: true);
    packageConfig.writeAsStringSync('''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "path_provider_linux",
      "rootUri": "../path_provider_linux",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ]
}
''');
    globals.fs.file('.packages').writeAsStringSync('''
path_provider_linux:/path_provider_linux/lib/
''');
    final Directory fakePluginDir = globals.fs.directory('path_provider_linux');
    final File pluginPubspec = fakePluginDir.childFile('pubspec.yaml');
    pluginPubspec.createSync(recursive: true);
    pluginPubspec.writeAsStringSync('''
name: path_provider_linux

flutter:
  plugin:
    implements: path_provider
    platforms:
      linux:
        dartPluginClass: PathProviderLinux
''');

    residentRunner = HotRunner(
        <FlutterDevice>[
          flutterDevice,
        ],
        stayResident: false,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
        target: 'custom_main.dart',
        devtoolsHandler: createNoOpHandler,
      );
    await residentRunner.runSourceGenerators();

    final File generatedMain = globals.fs.directory('.dart_tool')
        .childDirectory('flutter_build')
        .childFile('dart_plugin_registrant.dart');

    expect(generatedMain.existsSync(), isTrue);
    expect(testLogger.errorText, isEmpty);
    expect(testLogger.statusText, isEmpty);
  }));

  testUsingContext('ResidentRunner can run source generation - generation fails', () => testbed.run(() async {
    // Intentionally define arb file with wrong name. generate_localizations defaults
    // to app_en.arb.
    final File arbFile = globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
      ..createSync(recursive: true);
    arbFile.writeAsStringSync('''
{
  "helloWorld": "Hello, World!",
  "@helloWorld": {
    "description": "Sample description"
  }
}''');
    globals.fs.file('l10n.yaml').createSync();
    globals.fs.file('pubspec.yaml').writeAsStringSync('flutter:\n  generate: true\n');

    await residentRunner.runSourceGenerators();

    expect(testLogger.errorText, allOf(contains('Exception')));
    expect(testLogger.statusText, isEmpty);
  }));

  testUsingContext('ResidentRunner printHelpDetails hot runner', () => testbed.run(() {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);

    residentRunner.printHelp(details: true);

    final CommandHelp commandHelp = residentRunner.commandHelp;

    // supports service protocol
    expect(residentRunner.supportsServiceProtocol, true);
    // isRunningDebug
    expect(residentRunner.isRunningDebug, true);
    // does support SkSL
    expect(residentRunner.supportsWriteSkSL, true);
    // commands
    expect(testLogger.statusText, equals(
        <dynamic>[
          'Flutter run key commands.',
          commandHelp.r,
          commandHelp.R,
          commandHelp.v,
          commandHelp.s,
          commandHelp.w,
          commandHelp.t,
          commandHelp.L,
          commandHelp.S,
          commandHelp.U,
          commandHelp.i,
          commandHelp.p,
          commandHelp.I,
          commandHelp.o,
          commandHelp.b,
          commandHelp.P,
          commandHelp.a,
          commandHelp.M,
          commandHelp.g,
          commandHelp.j,
          commandHelp.hWithDetails,
          commandHelp.c,
          commandHelp.q,
          '',
          '💪 Running with sound null safety 💪',
          '',
          'An Observatory debugger and profiler on FakeDevice is available at: null',
          '',
        ].join('\n')
    ));
  }));

  testUsingContext('ResidentRunner printHelp hot runner', () => testbed.run(() {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);

    residentRunner.printHelp(details: false);

    final CommandHelp commandHelp = residentRunner.commandHelp;

    // supports service protocol
    expect(residentRunner.supportsServiceProtocol, true);
    // isRunningDebug
    expect(residentRunner.isRunningDebug, true);
    // does support SkSL
    expect(residentRunner.supportsWriteSkSL, true);
    // commands
    expect(testLogger.statusText, equals(
        <dynamic>[
          'Flutter run key commands.',
          commandHelp.r,
          commandHelp.R,
          commandHelp.hWithoutDetails,
          commandHelp.c,
          commandHelp.q,
          '',
          '💪 Running with sound null safety 💪',
          '',
          'An Observatory debugger and profiler on FakeDevice is available at: null',
          '',
        ].join('\n')
    ));
  }));

  testUsingContext('ResidentRunner printHelpDetails cold runner', () => testbed.run(() {
    fakeVmServiceHost = null;
    residentRunner = ColdRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.printHelp(details: true);

    final CommandHelp commandHelp = residentRunner.commandHelp;

    // does not supports service protocol
    expect(residentRunner.supportsServiceProtocol, false);
    // isRunningDebug
    expect(residentRunner.isRunningDebug, false);
    // does support SkSL
    expect(residentRunner.supportsWriteSkSL, false);
    // commands
    expect(testLogger.statusText, equals(
        <dynamic>[
          'Flutter run key commands.',
          commandHelp.v,
          commandHelp.s,
          commandHelp.hWithDetails,
          commandHelp.c,
          commandHelp.q,
          ''
        ].join('\n')
    ));
  }));

  testUsingContext('ResidentRunner printHelp cold runner', () => testbed.run(() {
    fakeVmServiceHost = null;
    residentRunner = ColdRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.printHelp(details: false);

    final CommandHelp commandHelp = residentRunner.commandHelp;

    // does not supports service protocol
    expect(residentRunner.supportsServiceProtocol, false);
    // isRunningDebug
    expect(residentRunner.isRunningDebug, false);
    // does support SkSL
    expect(residentRunner.supportsWriteSkSL, false);
    // commands
    expect(testLogger.statusText, equals(
        <dynamic>[
          'Flutter run key commands.',
          commandHelp.hWithoutDetails,
          commandHelp.c,
          commandHelp.q,
          ''
        ].join('\n')
    ));
  }));

  testUsingContext('ResidentRunner handles writeSkSL returning no data', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      FakeVmServiceRequest(
        method: kGetSkSLsMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
        },
        jsonResponse: <String, Object>{
          'SkSLs': <String, Object>{}
        }
      ),
    ]);
    await residentRunner.writeSkSL();

    expect(testLogger.statusText, contains('No data was received'));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('ResidentRunner can write SkSL data to a unique file with engine revision, platform, and device name', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      FakeVmServiceRequest(
        method: kGetSkSLsMethod,
        args: <String, Object>{
          'viewId': fakeFlutterView.id,
        },
        jsonResponse: <String, Object>{
          'SkSLs': <String, Object>{
            'A': 'B',
          }
        }
      )
    ]);
    await residentRunner.writeSkSL();

    expect(testLogger.statusText, contains('flutter_01.sksl.json'));
    expect(globals.fs.file('flutter_01.sksl.json'), exists);
    expect(json.decode(globals.fs.file('flutter_01.sksl.json').readAsStringSync()), <String, Object>{
      'platform': 'android',
      'name': 'FakeDevice',
      'engineRevision': 'abcdefg',
      'data': <String, Object>{'A': 'B'}
    });
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    FileSystemUtils: () => FileSystemUtils(
      fileSystem: globals.fs,
      platform: globals.platform,
    ),
    FlutterVersion: () => FakeFlutterVersion(engineRevision: 'abcdefg')
  }));

  testUsingContext('ResidentRunner ignores DevtoolsLauncher when attaching with enableDevTools: false - cold mode', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    residentRunner = ColdRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile, vmserviceOutFile: 'foo'),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    final Future<int> result = residentRunner.attach();
    expect(await result, 0);
  }));

  testUsingContext('FlutterDevice can exit from a release mode isolate with no VmService', () => testbed.run(() async {
    final TestFlutterDevice flutterDevice = TestFlutterDevice(
      device,
    );

    await flutterDevice.exitApps();

    expect(device.appStopped, true);
  }));

  testUsingContext('FlutterDevice will exit an un-paused isolate using stopApp', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final TestFlutterDevice flutterDevice = TestFlutterDevice(
      device,
    );
    flutterDevice.vmService = fakeVmServiceHost.vmService;

    final Future<void> exitFuture = flutterDevice.exitApps();

    await expectLater(exitFuture, completes);
    expect(device.appStopped, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('HotRunner writes vm service file when providing debugging option', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, vmserviceOutFile: 'foo'),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    await residentRunner.run(enableDevTools: true);

    expect(fakeVmServiceHost.hasRemainingExpectations, false);
    expect(await globals.fs.file('foo').readAsString(), testUri.toString());
  }));

  testUsingContext('HotRunner copies compiled app.dill to cache during startup', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(
        const BuildInfo(
          BuildMode.debug,
          null,
          treeShakeIcons: false,
        )
      ),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');

    await residentRunner.run(enableDevTools: true);

    expect(await globals.fs.file(globals.fs.path.join('build', 'cache.dill')).readAsString(), 'ABC');
  }));

  testUsingContext('HotRunner copies compiled app.dill to cache during startup with dart defines', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(
        const BuildInfo(
          BuildMode.debug,
          '',
          treeShakeIcons: false,
          dartDefines: <String>['a', 'b'],
        )
      ),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');

    await residentRunner.run(enableDevTools: true);

    expect(await globals.fs.file(globals.fs.path.join(
      'build', '187ef4436122d1cc2f40dc2b92f0eba0.cache.dill')).readAsString(), 'ABC');
  }));

  testUsingContext('HotRunner copies compiled app.dill to cache during startup with null safety', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(
        const BuildInfo(
          BuildMode.debug,
          '',
          treeShakeIcons: false,
          extraFrontEndOptions: <String>['--enable-experiment=non-nullable']
        )
      ),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');

    await residentRunner.run(enableDevTools: true);

    expect(await globals.fs.file(globals.fs.path.join(
      'build', 'cache.dill')).readAsString(), 'ABC');
  }));

  testUsingContext('HotRunner copies compiled app.dill to cache during startup with track-widget-creation', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');

    await residentRunner.run(enableDevTools: true);

    expect(await globals.fs.file(globals.fs.path.join(
      'build', 'cache.dill.track.dill')).readAsString(), 'ABC');
  }));

  testUsingContext('HotRunner does not copy app.dill if a dillOutputPath is given', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      dillOutputPath: 'test',
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');

    await residentRunner.run(enableDevTools: true);

    expect(globals.fs.file(globals.fs.path.join('build', 'cache.dill')), isNot(exists));
  }));

  testUsingContext('HotRunner copies compiled app.dill to cache during startup with --track-widget-creation', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        trackWidgetCreation: true,
      )),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    residentRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');

    await residentRunner.run(enableDevTools: true);

    expect(await globals.fs.file(globals.fs.path.join('build', 'cache.dill.track.dill')).readAsString(), 'ABC');
  }));

  testUsingContext('HotRunner calls device dispose', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    await residentRunner.run();
    expect(device.disposed, true);
  }));

  testUsingContext('HotRunner handles failure to write vmservice file', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      listViews,
    ]);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, vmserviceOutFile: 'foo'),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    await residentRunner.run(enableDevTools: true);

    expect(testLogger.errorText, contains('Failed to write vmservice-out-file at foo'));
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    FileSystem: () => ThrowingForwardingFileSystem(MemoryFileSystem.test()),
  }));

  testUsingContext('ColdRunner writes vm service file when providing debugging option', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
    ], wsAddress: testUri);
    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
    residentRunner = ColdRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile, vmserviceOutFile: 'foo'),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    await residentRunner.run(enableDevTools: true);

    expect(await globals.fs.file('foo').readAsString(), testUri.toString());
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('FlutterDevice uses dartdevc configuration when targeting web', () async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice(targetPlatform: TargetPlatform.web_javascript);
    final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
      device,
      buildInfo: const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        nullSafetyMode: NullSafetyMode.unsound,
      ),
      target: null,
      platform: FakePlatform(),
    )).generator as DefaultResidentCompiler;

    expect(residentCompiler.initializeFromDill,
      globals.fs.path.join(getBuildDirectory(), 'fbbe6a61fb7a1de317d381f8df4814e5.cache.dill'));
    expect(residentCompiler.librariesSpec,
      globals.fs.file(globals.artifacts.getHostArtifact(HostArtifact.flutterWebLibrariesJson))
        .uri.toString());
    expect(residentCompiler.targetModel, TargetModel.dartdevc);
    expect(residentCompiler.sdkRoot,
      '${globals.artifacts.getHostArtifact(HostArtifact.flutterWebSdk).path}/');
    expect(residentCompiler.platformDill, 'file:///HostArtifact.webPlatformKernelDill');
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('FlutterDevice uses dartdevc configuration when targeting web with null-safety autodetected', () async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice(targetPlatform: TargetPlatform.web_javascript);

    final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
      device,
      buildInfo: const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        extraFrontEndOptions: <String>['--enable-experiment=non-nullable'],
      ),
      target: null,
      platform: FakePlatform(),
    )).generator as DefaultResidentCompiler;

    expect(residentCompiler.initializeFromDill,
      globals.fs.path.join(getBuildDirectory(), '80b1a4cf4e7b90e1ab5f72022a0bc624.cache.dill'));
    expect(residentCompiler.librariesSpec,
      globals.fs.file(globals.artifacts.getHostArtifact(HostArtifact.flutterWebLibrariesJson))
        .uri.toString());
    expect(residentCompiler.targetModel, TargetModel.dartdevc);
    expect(residentCompiler.sdkRoot,
      '${globals.artifacts.getHostArtifact(HostArtifact.flutterWebSdk).path}/');
    expect(residentCompiler.platformDill, 'file:///HostArtifact.webPlatformSoundKernelDill');
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('FlutterDevice passes flutter-widget-cache flag when feature is enabled', () async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice();

    final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
      device,
      buildInfo: const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        extraFrontEndOptions: <String>[],
      ),
      target: null, platform: null,
    )).generator as DefaultResidentCompiler;

    expect(residentCompiler.extraFrontEndOptions,
      contains('--flutter-widget-cache'));
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
    FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true)
  });

   testUsingContext('FlutterDevice passes alternative-invalidation-strategy flag', () async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice();


    final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
      device,
      buildInfo: const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        extraFrontEndOptions: <String>[],
      ),
      target: null, platform: null,
    )).generator as DefaultResidentCompiler;

    expect(residentCompiler.extraFrontEndOptions,
      contains('--enable-experiment=alternative-invalidation-strategy'));
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

   testUsingContext('FlutterDevice passes initializeFromDill parameter if specified', () async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice();

    final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
      device,
      buildInfo: const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        extraFrontEndOptions: <String>[],
        initializeFromDill: '/foo/bar.dill',
      ),
      target: null, platform: null,
    )).generator as DefaultResidentCompiler;

    expect(residentCompiler.initializeFromDill, '/foo/bar.dill');
    expect(residentCompiler.assumeInitializeFromDillUpToDate, false);
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

   testUsingContext('FlutterDevice passes assumeInitializeFromDillUpToDate parameter if specified', () async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice();

    final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
      device,
      buildInfo: const BuildInfo(
        BuildMode.debug,
        '',
        treeShakeIcons: false,
        extraFrontEndOptions: <String>[],
        assumeInitializeFromDillUpToDate: true,
      ),
      target: null, platform: null,
    )).generator as DefaultResidentCompiler;

    expect(residentCompiler.assumeInitializeFromDillUpToDate, true);
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('Handle existing VM service clients DDS error', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice()
      ..dds = DartDevelopmentService();
    ddsLauncherCallback = (Uri uri, {bool enableAuthCodes, bool ipv6, Uri serviceUri, List<String> cachedUserTags}) {
      expect(uri, Uri(scheme: 'foo', host: 'bar'));
      expect(enableAuthCodes, isTrue);
      expect(ipv6, isFalse);
      expect(serviceUri, Uri(scheme: 'http', host: '127.0.0.1', port: 0));
      expect(cachedUserTags, isEmpty);
      throw FakeDartDevelopmentServiceException(message:
        'Existing VM service clients prevent DDS from taking control.',
      );
    };
    final TestFlutterDevice flutterDevice = TestFlutterDevice(
      device,
      observatoryUris: Stream<Uri>.value(testUri),
    );
    bool caught = false;
    final Completer<void>done = Completer<void>();
    runZonedGuarded(() {
      flutterDevice.connect(allowExistingDdsInstance: true).then((_) => done.complete());
    }, (Object e, StackTrace st) {
      expect(e is ToolExit, true);
      expect((e as ToolExit).message,
        contains('Existing VM service clients prevent DDS from taking control.',
      ));
      done.complete();
      caught = true;
    });
    await done.future;
    if (!caught) {
      fail('Expected ToolExit to be thrown.');
    }
  }, overrides: <Type, Generator>{
    VMServiceConnector: () => (Uri httpUri, {
      ReloadSources reloadSources,
      Restart restart,
      CompileExpression compileExpression,
      GetSkSLMethod getSkSLMethod,
      PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
      io.CompressionOptions compression,
      Device device,
      Logger logger,
    }) async => FakeVmServiceHost(requests: <VmServiceExpectation>[]).vmService,
  }));

  testUsingContext('Host VM service ipv6 defaults', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    final FakeDevice device = FakeDevice()
      ..dds = DartDevelopmentService();
    final Completer<void>done = Completer<void>();
    ddsLauncherCallback = (Uri uri, {bool enableAuthCodes, bool ipv6, Uri serviceUri, List<String> cachedUserTags}) async {
      expect(uri, Uri(scheme: 'foo', host: 'bar'));
      expect(enableAuthCodes, isFalse);
      expect(ipv6, isTrue);
      expect(serviceUri, Uri(scheme: 'http', host: '::1', port: 0));
      expect(cachedUserTags, isEmpty);
      done.complete();
      return null;
    };
    final TestFlutterDevice flutterDevice = TestFlutterDevice(
      device,
      observatoryUris: Stream<Uri>.value(testUri),
    );
    await flutterDevice.connect(allowExistingDdsInstance: true, ipv6: true, disableServiceAuthCodes: true);
    await done.future;
  }, overrides: <Type, Generator>{
    VMServiceConnector: () => (Uri httpUri, {
      ReloadSources reloadSources,
      Restart restart,
      CompileExpression compileExpression,
      GetSkSLMethod getSkSLMethod,
      PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
      io.CompressionOptions compression,
      Device device,
      Logger logger,
    }) async => FakeVmServiceHost(requests: <VmServiceExpectation>[]).vmService,
  }));

  testUsingContext('Failed DDS start outputs error message', () => testbed.run(() async {
    // See https://github.com/flutter/flutter/issues/72385 for context.
    final FakeDevice device = FakeDevice()
      ..dds = DartDevelopmentService();
    ddsLauncherCallback = (Uri uri, {bool enableAuthCodes, bool ipv6, Uri serviceUri, List<String> cachedUserTags}) {
      expect(uri, Uri(scheme: 'foo', host: 'bar'));
      expect(enableAuthCodes, isTrue);
      expect(ipv6, isFalse);
      expect(serviceUri, Uri(scheme: 'http', host: '127.0.0.1', port: 0));
      expect(cachedUserTags, isEmpty);
      throw FakeDartDevelopmentServiceException(message: 'No URI');
    };
    final TestFlutterDevice flutterDevice = TestFlutterDevice(
      device,
      observatoryUris: Stream<Uri>.value(testUri),
    );
    bool caught = false;
    final Completer<void>done = Completer<void>();
    runZonedGuarded(() {
      flutterDevice.connect(allowExistingDdsInstance: true).then((_) => done.complete());
    }, (Object e, StackTrace st) {
      expect(e is StateError, true);
      expect((e as StateError).message, contains('No URI'));
      expect(testLogger.errorText, contains(
        'DDS has failed to start and there is not an existing DDS instance',
      ));
      done.complete();
      caught = true;
    });
    await done.future;
    if (!caught) {
      fail('Expected a StateError to be thrown.');
    }
  }, overrides: <Type, Generator>{
    VMServiceConnector: () => (Uri httpUri, {
      ReloadSources reloadSources,
      Restart restart,
      CompileExpression compileExpression,
      GetSkSLMethod getSkSLMethod,
      PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
      io.CompressionOptions compression,
      Device device,
      Logger logger,
    }) async => FakeVmServiceHost(requests: <VmServiceExpectation>[]).vmService,
  }));

  testUsingContext('nextPlatform moves through expected platforms', () {
    expect(nextPlatform('android'), 'iOS');
    expect(nextPlatform('iOS'), 'fuchsia');
    expect(nextPlatform('fuchsia'), 'macOS');
    expect(nextPlatform('macOS'), 'android');
    expect(() => nextPlatform('unknown'), throwsAssertionError);
  });

  testUsingContext('cleanupAtFinish shuts down resident devtools handler', () => testbed.run(() async {
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, vmserviceOutFile: 'foo'),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );
    await residentRunner.cleanupAtFinish();

    expect((residentRunner.residentDevtoolsHandler as NoOpDevtoolsHandler).wasShutdown, true);
  }));

  testUsingContext('HotRunner sets asset directory when first evict assets', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      setAssetBundlePath,
      evict,
    ]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    (flutterDevice.devFS as FakeDevFS).assetPathsToEvict = <String>{'asset'};

    expect(flutterDevice.devFS.hasSetAssetDirectory, false);
    await (residentRunner as HotRunner).evictDirtyAssets();
    expect(flutterDevice.devFS.hasSetAssetDirectory, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('HotRunner does not sets asset directory when no assets to evict', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
    ]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    expect(flutterDevice.devFS.hasSetAssetDirectory, false);
    await (residentRunner as HotRunner).evictDirtyAssets();
    expect(flutterDevice.devFS.hasSetAssetDirectory, false);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));

  testUsingContext('HotRunner does not set asset directory if it has been set before', () => testbed.run(() async {
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      listViews,
      evict,
    ]);
    residentRunner = HotRunner(
      <FlutterDevice>[
        flutterDevice,
      ],
      stayResident: false,
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      target: 'main.dart',
      devtoolsHandler: createNoOpHandler,
    );

    (flutterDevice.devFS as FakeDevFS).assetPathsToEvict = <String>{'asset'};
    flutterDevice.devFS.hasSetAssetDirectory = true;

    await (residentRunner as HotRunner).evictDirtyAssets();
    expect(flutterDevice.devFS.hasSetAssetDirectory, true);
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }));
}

class FakeDartDevelopmentServiceException implements dds.DartDevelopmentServiceException {
  FakeDartDevelopmentServiceException({this.message = defaultMessage});

  @override
  final int errorCode = dds.DartDevelopmentServiceException.existingDdsInstanceError;

  @override
  final String message;
  static const String defaultMessage = 'A DDS instance is already connected at http://localhost:8181';
}

class TestFlutterDevice extends FlutterDevice {
  TestFlutterDevice(Device device, { Stream<Uri> observatoryUris })
    : super(device, buildInfo: BuildInfo.debug) {
    _observatoryUris = observatoryUris;
  }

  @override
  Stream<Uri> get observatoryUris => _observatoryUris;
  Stream<Uri> _observatoryUris;
}

class ThrowingForwardingFileSystem extends ForwardingFileSystem {
  ThrowingForwardingFileSystem(FileSystem delegate) : super(delegate);

  @override
  File file(dynamic path) {
    if (path == 'foo') {
      throw const FileSystemException();
    }
    return delegate.file(path);
  }
}

class FakeFlutterDevice extends Fake implements FlutterDevice {
  FakeVmServiceHost Function() vmServiceHost;
  Uri testUri;
  UpdateFSReport report = UpdateFSReport(
    success: true,
    invalidatedSourcesCount: 1,
  );
  Exception reportError;
  Exception runColdError;
  int runHotCode = 0;
  int runColdCode = 0;

  @override
  ResidentCompiler generator;

  @override
  Stream<Uri> get observatoryUris => Stream<Uri>.value(testUri);

  @override
  FlutterVmService get vmService => vmServiceHost?.call()?.vmService;

  DevFS _devFS;

  @override
  DevFS get devFS => _devFS;

  @override
  set devFS(DevFS value) { }

  @override
  Device device;

  @override
  Future<void> stopEchoingDeviceLog() async { }

  @override
  Future<void> initLogReader() async { }

  @override
  Future<Uri> setupDevFS(String fsName, Directory rootDirectory) async {
    return testUri;
  }

  @override
  Future<int> runHot({HotRunner hotRunner, String route}) async {
    return runHotCode;
  }

  @override
  Future<int> runCold({ColdRunner coldRunner, String route}) async {
    if (runColdError != null) {
      throw runColdError;
    }
    return runColdCode;
  }

  @override
  Future<void> connect({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
    GetSkSLMethod getSkSLMethod,
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
    int hostVmServicePort,
    int ddsPort,
    bool disableServiceAuthCodes = false,
    bool enableDds = true,
    bool cacheStartupProfile = false,
    @required bool allowExistingDdsInstance,
    bool ipv6 = false,
  }) async { }

  @override
  Future<UpdateFSReport> updateDevFS({
    Uri mainUri,
    String target,
    AssetBundle bundle,
    DateTime firstBuildTime,
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
    String projectRootPath,
    String pathToReload,
    String dillOutputPath,
    List<Uri> invalidatedFiles,
    PackageConfig packageConfig,
  }) async {
    if (reportError != null) {
      throw reportError;
    }
    return report;
  }

  @override
  Future<void> updateReloadStatus(bool wasReloadSuccessful) async { }
}

class FakeDelegateFlutterDevice extends FlutterDevice {
  FakeDelegateFlutterDevice(
    Device device,
    BuildInfo buildInfo,
    ResidentCompiler residentCompiler,
    this.fakeDevFS,
  ) : super(device, buildInfo: buildInfo, generator: residentCompiler);

  @override
  Future<void> connect({
    ReloadSources reloadSources,
    Restart restart,
    bool enableDds = true,
    bool cacheStartupProfile = false,
    bool disableServiceAuthCodes = false,
    bool ipv6 = false,
    CompileExpression compileExpression,
    GetSkSLMethod getSkSLMethod,
    int hostVmServicePort,
    int ddsPort,
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
    bool allowExistingDdsInstance = false,
  }) async { }


  final DevFS fakeDevFS;

  @override
  DevFS get devFS => fakeDevFS;

  @override
  set devFS(DevFS value) {}
}

class FakeResidentCompiler extends Fake implements ResidentCompiler {
  CompilerOutput nextOutput;
  bool didSuppressErrors = false;

  @override
  Future<CompilerOutput> recompile(
    Uri mainUri,
    List<Uri> invalidatedFiles, {
    @required String outputPath,
    @required PackageConfig packageConfig,
    @required String projectRootPath,
    @required FileSystem fs,
    bool suppressErrors = false,
    bool checkDartPluginRegistry = false,
  }) async {
    didSuppressErrors = suppressErrors;
    return nextOutput ?? const CompilerOutput('foo.dill', 0, <Uri>[]);
  }

  @override
  void accept() { }

  @override
  void reset() { }
}

class FakeProjectFileInvalidator extends Fake implements ProjectFileInvalidator {
  @override
  Future<InvalidationResult> findInvalidated({
    @required DateTime lastCompiled,
    @required List<Uri> urisToMonitor,
    @required String packagesPath,
    @required PackageConfig packageConfig,
    bool asyncScanning = false,
  }) async {
    return InvalidationResult(
      packageConfig: packageConfig ?? PackageConfig.empty,
      uris: <Uri>[Uri.parse('file:///hello_world/main.dart'),
    ]);
  }
}

// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device {
  FakeDevice({
    String sdkNameAndVersion = 'Android',
    TargetPlatform targetPlatform = TargetPlatform.android_arm,
    bool isLocalEmulator = false,
    this.supportsHotRestart = true,
    this.supportsScreenshot = true,
    this.supportsFlutterExit = true,
  }) : _isLocalEmulator = isLocalEmulator,
       _targetPlatform = targetPlatform,
       _sdkNameAndVersion = sdkNameAndVersion;

  final bool _isLocalEmulator;
  final TargetPlatform _targetPlatform;
  final String _sdkNameAndVersion;

  bool disposed = false;
  bool appStopped = false;
  bool failScreenshot = false;

  @override
  bool supportsHotRestart;

  @override
  bool supportsScreenshot;

  @override
  bool supportsFlutterExit;

  @override
  PlatformType get platformType => _targetPlatform == TargetPlatform.web_javascript
    ? PlatformType.web
    : PlatformType.android;

  @override
  Future<String> get sdkNameAndVersion async => _sdkNameAndVersion;

  @override
  Future<TargetPlatform> get targetPlatform async => _targetPlatform;

  @override
  Future<bool> get isLocalEmulator async => _isLocalEmulator;

  @override
  String get name => 'FakeDevice';

  @override
  DartDevelopmentService dds;

  @override
  Future<void> dispose() async {
    disposed = true;
  }

  @override
  Future<bool> stopApp(covariant ApplicationPackage app, {String userIdentifier}) async {
    appStopped = true;
    return true;
  }

  @override
  Future<void> takeScreenshot(File outputFile) async {
    if (failScreenshot) {
      throw Exception();
    }
    outputFile.writeAsBytesSync(List<int>.generate(1024, (int i) => i));
  }

  @override
  FutureOr<DeviceLogReader> getLogReader({
    covariant ApplicationPackage app,
    bool includePastLogs = false,
  }) => NoOpDeviceLogReader(name);

  @override
  DevicePortForwarder portForwarder = const NoOpDevicePortForwarder();
}

class FakeDevFS extends Fake implements DevFS {
  @override
  DateTime lastCompiled = DateTime(2000);

  @override
  PackageConfig lastPackageConfig = PackageConfig.empty;

  @override
  List<Uri> sources = <Uri>[];

  @override
  Uri baseUri = Uri();

  @override
  Future<void> destroy() async { }

  @override
  Set<String> assetPathsToEvict = <String>{};

  UpdateFSReport nextUpdateReport = UpdateFSReport(success: true);

  @override
  bool hasSetAssetDirectory = false;

  @override
  Future<Uri> create() async {
    return Uri();
  }

  @override
  void resetLastCompiled() {
    lastCompiled = null;
  }

  @override
  Future<UpdateFSReport> update({
    @required Uri mainUri,
    @required ResidentCompiler generator,
    @required bool trackWidgetCreation,
    @required String pathToReload,
    @required List<Uri> invalidatedFiles,
    @required PackageConfig packageConfig,
    @required String dillOutputPath,
    DevFSWriter devFSWriter,
    String target,
    AssetBundle bundle,
    DateTime firstBuildTime,
    bool bundleFirstUpload = false,
    bool fullRestart = false,
    String projectRootPath,
  }) async {
    return nextUpdateReport;
  }
}
