resident_web_runner_test.dart 56.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert';
7
import 'dart:io';
8 9

import 'package:dwds/dwds.dart';
10
import 'package:file/memory.dart';
11
import 'package:flutter_tools/src/application_package.dart';
12
import 'package:flutter_tools/src/asset.dart';
13
import 'package:flutter_tools/src/base/dds.dart';
14
import 'package:flutter_tools/src/base/file_system.dart';
15
import 'package:flutter_tools/src/base/logger.dart';
16
import 'package:flutter_tools/src/base/platform.dart';
17
import 'package:flutter_tools/src/base/time.dart';
18
import 'package:flutter_tools/src/build_info.dart';
19
import 'package:flutter_tools/src/build_system/targets/scene_importer.dart';
20
import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart';
21 22
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
23
import 'package:flutter_tools/src/device.dart';
24
import 'package:flutter_tools/src/globals.dart' as globals;
25 26
import 'package:flutter_tools/src/isolated/devfs_web.dart';
import 'package:flutter_tools/src/isolated/resident_web_runner.dart';
27
import 'package:flutter_tools/src/project.dart';
28
import 'package:flutter_tools/src/reporting/reporting.dart';
29
import 'package:flutter_tools/src/resident_devtools_handler.dart';
30
import 'package:flutter_tools/src/resident_runner.dart';
31
import 'package:flutter_tools/src/vmservice.dart';
32
import 'package:flutter_tools/src/web/chrome.dart';
33
import 'package:flutter_tools/src/web/web_device.dart';
34 35
import 'package:package_config/package_config.dart';
import 'package:package_config/package_config_types.dart';
36
import 'package:test/fake.dart';
37
import 'package:vm_service/vm_service.dart' as vm_service;
38
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
39 40

import '../src/common.dart';
41
import '../src/context.dart';
42
import '../src/fake_vm_services.dart';
43

44 45
const List<VmServiceExpectation> kAttachLogExpectations =
    <VmServiceExpectation>[
46 47 48 49 50 51 52 53 54 55 56
  FakeVmServiceRequest(
    method: 'streamListen',
    args: <String, Object>{
      'streamId': 'Stdout',
    },
  ),
  FakeVmServiceRequest(
    method: 'streamListen',
    args: <String, Object>{
      'streamId': 'Stderr',
    },
57
  ),
58 59
];

60 61 62 63 64 65
const List<VmServiceExpectation> kAttachIsolateExpectations =
    <VmServiceExpectation>[
  FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
    'streamId': 'Isolate',
  }),
  FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
66 67
    'service': kReloadSourcesServiceName,
    'alias': kFlutterToolAlias,
68 69
  }),
  FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
70 71
    'service': kFlutterVersionServiceName,
    'alias': kFlutterToolAlias,
72 73
  }),
  FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
74 75
    'service': kFlutterMemoryInfoServiceName,
    'alias': kFlutterToolAlias,
76
  }),
77
  FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
78 79 80 81 82 83
    'service': kFlutterGetIOSBuildOptionsServiceName,
    'alias': kFlutterToolAlias,
  }),
  FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
    'service': kFlutterGetAndroidBuildVariantsServiceName,
    'alias': kFlutterToolAlias,
84
  }),
85
  FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
86 87
    'service': kFlutterGetIOSUniversalLinkSettingsServiceName,
    'alias': kFlutterToolAlias,
88
  }),
89 90 91 92 93 94
  FakeVmServiceRequest(
    method: 'streamListen',
    args: <String, Object>{
      'streamId': 'Extension',
    },
  ),
95 96 97 98 99 100 101
];

const List<VmServiceExpectation> kAttachExpectations = <VmServiceExpectation>[
  ...kAttachLogExpectations,
  ...kAttachIsolateExpectations,
];

102
void main() {
103 104 105 106 107 108 109 110 111 112 113 114 115 116
  late FakeDebugConnection debugConnection;
  late FakeChromeDevice chromeDevice;
  late FakeAppConnection appConnection;
  late FakeFlutterDevice flutterDevice;
  late FakeWebDevFS webDevFS;
  late FakeResidentCompiler residentCompiler;
  late FakeChromeConnection chromeConnection;
  late FakeChromeTab chromeTab;
  late FakeWebServerDevice webServerDevice;
  late FakeDevice mockDevice;
  late FakeVmServiceHost fakeVmServiceHost;
  late FileSystem fileSystem;
  late ProcessManager processManager;
  late TestUsage testUsage;
117 118

  setUp(() {
119
    testUsage = TestUsage();
120 121
    fileSystem = MemoryFileSystem.test();
    processManager = FakeProcessManager.any();
122 123 124
    debugConnection = FakeDebugConnection();
    mockDevice = FakeDevice();
    appConnection = FakeAppConnection();
125 126
    webDevFS = FakeWebDevFS();
    residentCompiler = FakeResidentCompiler();
127
    chromeDevice = FakeChromeDevice();
128 129
    chromeConnection = FakeChromeConnection();
    chromeTab = FakeChromeTab('index.html');
130 131 132 133 134
    webServerDevice = FakeWebServerDevice();
    flutterDevice = FakeFlutterDevice()
      .._devFS = webDevFS
      ..device = mockDevice
      ..generator = residentCompiler;
135
    fileSystem.file('.packages').writeAsStringSync('\n');
136 137
  });

138
  void setupMocks() {
139 140 141
    fileSystem.file('pubspec.yaml').createSync();
    fileSystem.file('lib/main.dart').createSync(recursive: true);
    fileSystem.file('web/index.html').createSync(recursive: true);
142
    webDevFS.report = UpdateFSReport(success: true);
143
    debugConnection.fakeVmServiceHost = () => fakeVmServiceHost;
144
    webDevFS.result = ConnectionResult(
145 146 147
      appConnection,
      debugConnection,
      debugConnection.vmService,
148
    );
149
    debugConnection.uri = 'ws://127.0.0.1/abcd/';
150
    chromeConnection.tabs.add(chromeTab);
151 152
  }

153 154 155
  testUsingContext(
      'runner with web server device does not support debugging without --start-paused',
      () {
156 157
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
    flutterDevice.device = WebServerDevice(
158
      logger: BufferLogger.test(),
159
    );
160
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
161
    final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
162
      flutterDevice,
163 164
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
165 166
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
167 168 169 170 171
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
172 173

    expect(profileResidentWebRunner.debuggingEnabled, false);
174

175
    flutterDevice.device = chromeDevice;
176

177
    expect(residentWebRunner.debuggingEnabled, true);
178
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
179 180 181 182
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
183

184 185 186
  testUsingContext(
      'runner with web server device supports debugging with --start-paused',
      () {
187
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
188
    setupMocks();
189
    flutterDevice.device = WebServerDevice(
190
      logger: BufferLogger.test(),
191
    );
192
    final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
193
      flutterDevice,
194 195 196 197
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
      debuggingOptions:
          DebuggingOptions.enabled(BuildInfo.debug, startPaused: true),
198
      ipv6: true,
199 200 201 202
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
203 204
    );

205
    expect(profileResidentWebRunner.uri, webDevFS.baseUri);
206
    expect(profileResidentWebRunner.debuggingEnabled, true);
207 208 209 210 211
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
  testUsingContext('profile does not supportsServiceProtocol', () {
212
    final ResidentRunner residentWebRunner = ResidentWebRunner(
213
      flutterDevice,
214 215
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
216 217
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
218 219 220 221 222
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
223
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
224
    flutterDevice.device = chromeDevice;
225
    final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
226
      flutterDevice,
227 228
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
229 230
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile),
      ipv6: true,
231 232 233 234
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
235 236 237 238
    );

    expect(profileResidentWebRunner.supportsServiceProtocol, false);
    expect(residentWebRunner.supportsServiceProtocol, true);
239 240 241 242
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
243

244
  testUsingContext('Can successfully run and connect to vmservice', () async {
245
    final BufferLogger logger = BufferLogger.test();
246 247 248 249
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
250
    setupMocks();
251

252 253
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
254 255 256
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
257 258
    final DebugConnectionInfo debugConnectionInfo =
        await connectionInfoCompleter.future;
259

260
    expect(appConnection.ranMain, true);
261 262
    expect(logger.statusText,
        contains('Debug service listening on ws://127.0.0.1/abcd/'));
263
    expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/');
264
  }, overrides: <Type, Generator>{
265 266 267
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
268

269 270
  testUsingContext('WebRunner copies compiled app.dill to cache during startup',
      () async {
271 272 273
    final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(
      const BuildInfo(BuildMode.debug, null, treeShakeIcons: false),
    );
274 275 276 277
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, debuggingOptions: debuggingOptions);
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
278
    setupMocks();
279

280 281 282 283 284
    residentWebRunner.artifactDirectory
        .childFile('app.dill')
        .writeAsStringSync('ABC');
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
285 286 287 288 289
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

290 291 292 293 294
    expect(
        await fileSystem
            .file(fileSystem.path.join('build', 'cache.dill'))
            .readAsString(),
        'ABC');
295 296 297 298
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
299

300 301 302
  testUsingContext(
      'WebRunner copies compiled app.dill to cache during startup with track-widget-creation',
      () async {
303
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
304 305
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
306
    setupMocks();
307

308 309 310 311 312
    residentWebRunner.artifactDirectory
        .childFile('app.dill')
        .writeAsStringSync('ABC');
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
313 314 315 316 317
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

318 319 320 321 322
    expect(
        await fileSystem
            .file(fileSystem.path.join('build', 'cache.dill.track.dill'))
            .readAsString(),
        'ABC');
323 324 325 326 327
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

328
  // Regression test for https://github.com/flutter/flutter/issues/60613
329 330 331 332 333
  testUsingContext(
      'ResidentWebRunner calls appFailedToStart if initial compilation fails',
      () async {
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
334
    setupMocks();
335
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
336 337 338
    fileSystem
        .file(globals.fs.path.join('lib', 'main.dart'))
        .createSync(recursive: true);
339
    webDevFS.report = UpdateFSReport();
340 341 342 343 344 345 346 347 348

    expect(await residentWebRunner.run(), 1);
    // Completing this future ensures that the daemon can exit correctly.
    expect(await residentWebRunner.waitForAppToFinish(), 1);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

349 350 351
  testUsingContext(
      'Can successfully run without an index.html including status warning',
      () async {
352
    final BufferLogger logger = BufferLogger.test();
353 354
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
355
    setupMocks();
356
    fileSystem.directory('web').deleteSync(recursive: true);
357
    final ResidentWebRunner residentWebRunner = ResidentWebRunner(
358
      flutterDevice,
359 360
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
361 362 363
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
      stayResident: false,
364 365 366 367 368
      fileSystem: fileSystem,
      logger: logger,
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
369 370

    expect(await residentWebRunner.run(), 0);
371
    expect(logger.statusText,
372
        contains('This application is not configured to build on the web'));
373 374 375 376
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
377

378 379 380 381
  testUsingContext('Can successfully run and disconnect with --no-resident',
      () async {
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
382
    setupMocks();
383
    final ResidentRunner residentWebRunner = ResidentWebRunner(
384
      flutterDevice,
385 386
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
387 388 389
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
      stayResident: false,
390 391 392 393 394
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
395 396

    expect(await residentWebRunner.run(), 0);
397 398 399 400
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
401

402 403
  testUsingContext('Listens to stdout and stderr streams before running main',
      () async {
404
    final BufferLogger logger = BufferLogger.test();
405 406
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
407 408 409 410 411
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachLogExpectations,
      FakeVmServiceStreamResponse(
        streamId: 'Stdout',
        event: vm_service.Event(
412 413 414
            timestamp: 0,
            kind: vm_service.EventStreams.kStdout,
            bytes: base64.encode(utf8.encode('THIS MESSAGE IS IMPORTANT'))),
415 416 417 418
      ),
      FakeVmServiceStreamResponse(
        streamId: 'Stderr',
        event: vm_service.Event(
419 420 421
            timestamp: 0,
            kind: vm_service.EventStreams.kStderr,
            bytes: base64.encode(utf8.encode('SO IS THIS'))),
422 423 424
      ),
      ...kAttachIsolateExpectations,
    ]);
425
    setupMocks();
426 427
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
428 429 430 431 432
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

433 434
    expect(logger.statusText, contains('THIS MESSAGE IS IMPORTANT'));
    expect(logger.statusText, contains('SO IS THIS'));
435 436 437 438
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
439

440 441 442 443
  testUsingContext('Listens to extension events with structured errors',
      () async {
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: testLogger);
444
    final List<VmServiceExpectation> requests = <VmServiceExpectation>[
445
      ...kAttachExpectations,
446
      // This is the first error message of a session.
447 448 449 450 451
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Flutter.Error',
452 453 454 455
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'errorsSinceReload': 0,
            'renderedErrorText': 'first',
          }),
456 457 458
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
459
      // This is the second error message of a session.
460 461 462 463 464
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Flutter.Error',
465 466 467 468
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'errorsSinceReload': 1,
            'renderedErrorText': 'second',
          }),
469 470 471
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
472
      // This is not Flutter.Error kind data, so it should not be logged, even though it has a renderedErrorText field.
473 474 475 476 477
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Other',
478 479 480 481
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'errorsSinceReload': 2,
            'renderedErrorText': 'not an error',
          }),
482 483 484
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
      // This is the third error message of a session.
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Flutter.Error',
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'errorsSinceReload': 2,
            'renderedErrorText': 'third',
          }),
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
      // This is bogus error data.
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Flutter.Error',
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'other': 'bad stuff',
          }),
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
      // Empty error text should not break anything.
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Flutter.Error',
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'test': 'data',
            'renderedErrorText': '',
          }),
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
      // Messages without errorsSinceReload should act as if errorsSinceReload: 0
      FakeVmServiceStreamResponse(
        streamId: 'Extension',
        event: vm_service.Event(
          timestamp: 0,
          extensionKind: 'Flutter.Error',
          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
            'test': 'data',
            'renderedErrorText': 'error text',
          }),
          kind: vm_service.EventStreams.kExtension,
        ),
      ),
      // When adding things here, make sure the last one is supposed to output something
      // to the statusLog, otherwise you won't be able to distinguish the absence of output
      // due to it passing the test from absence due to it not running the test.
    ];
    // We use requests below, so make a copy of it here (FakeVmServiceHost will
    // clear its copy internally, which would affect our pumping below).
    fakeVmServiceHost = FakeVmServiceHost(requests: requests.toList());
543

544
    setupMocks();
545 546
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
547 548 549 550
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
    assert(requests.length > 5, 'requests was modified');
    for (final Object _ in requests) {
      // pump the task queue once for each message
      await null;
    }

    expect(testLogger.statusText,
      'Launching lib/main.dart on FakeDevice in debug mode...\n'
      'Waiting for connection from debug service on FakeDevice...\n'
      'Debug service listening on ws://127.0.0.1/abcd/\n'
      '\n'
      'first\n'
      '\n'
      'second\n'
      'third\n'
      '\n'
      '\n' // the empty message
      '\n'
      '\n'
      'error text\n'
      '\n'
    );
573

574 575 576
    expect(testLogger.errorText,
      'Received an invalid Flutter.Error message from app: {other: bad stuff}\n'
    );
577 578 579 580
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
581

582
  testUsingContext('Does not run main with --start-paused', () async {
583
    final ResidentRunner residentWebRunner = ResidentWebRunner(
584
      flutterDevice,
585 586 587 588
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
      debuggingOptions:
          DebuggingOptions.enabled(BuildInfo.debug, startPaused: true),
589
      ipv6: true,
590 591 592 593 594
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
595 596
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
597
    setupMocks();
598 599
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
600

601 602 603 604 605
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

606
    expect(appConnection.ranMain, false);
607 608 609 610
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
611

612
  testUsingContext('Can hot reload after attaching', () async {
613 614
    final BufferLogger logger = BufferLogger.test();
    final ResidentRunner residentWebRunner = setUpResidentRunner(
615
      flutterDevice,
616
      logger: logger,
617
      systemClock: SystemClock.fixed(DateTime(2001)),
618
    );
619 620 621
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
      const FakeVmServiceRequest(
622
          method: kHotRestartServiceName,
623 624 625
          jsonResponse: <String, Object>{
            'type': 'Success',
          }),
626 627 628 629 630 631
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
632
    ]);
633
    setupMocks();
634
    final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
635 636
    final Chromium chrome =
        Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
637
    chromiumLauncher.setInstance(chrome);
638

639
    flutterDevice.device = GoogleChromeDevice(
640
      fileSystem: fileSystem,
641
      chromiumLauncher: chromiumLauncher,
642
      logger: BufferLogger.test(),
643
      platform: FakePlatform(),
644
      processManager: FakeProcessManager.any(),
645
    );
646 647
    webDevFS.report = UpdateFSReport(success: true);

648 649
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
650 651 652
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
653 654
    final DebugConnectionInfo debugConnectionInfo =
        await connectionInfoCompleter.future;
655 656 657

    expect(debugConnectionInfo, isNotNull);

658
    final OperationResult result = await residentWebRunner.restart();
659

660
    expect(logger.statusText, contains('Restarted application in'));
661
    expect(result.code, 0);
662
    expect(webDevFS.mainUri.toString(), contains('entrypoint.dart'));
663 664

    // ensure that analytics are sent.
665
    expect(testUsage.events, <TestUsageEvent>[
666 667 668 669 670 671 672 673 674
      TestUsageEvent('hot', 'restart',
          parameters: CustomDimensions.fromMap(<String, String>{
            'cd27': 'web-javascript',
            'cd28': '',
            'cd29': 'false',
            'cd30': 'true',
            'cd13': '0',
            'cd48': 'false'
          })),
675 676 677 678
    ]);
    expect(testUsage.timings, const <TestTimingEvent>[
      TestTimingEvent('hot', 'web-incremental-restart', Duration.zero),
    ]);
679
  }, overrides: <Type, Generator>{
680
    Usage: () => testUsage,
681 682 683
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
684

685
  testUsingContext('Can hot restart after attaching', () async {
686 687
    final BufferLogger logger = BufferLogger.test();
    final ResidentRunner residentWebRunner = setUpResidentRunner(
688
      flutterDevice,
689
      logger: logger,
690
      systemClock: SystemClock.fixed(DateTime(2001)),
691
    );
692 693 694
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
      const FakeVmServiceRequest(
695
          method: kHotRestartServiceName,
696 697 698
          jsonResponse: <String, Object>{
            'type': 'Success',
          }),
699
    ]);
700
    setupMocks();
701
    final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
702 703
    final Chromium chrome =
        Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
704
    chromiumLauncher.setInstance(chrome);
705

706
    flutterDevice.device = GoogleChromeDevice(
707
      fileSystem: fileSystem,
708
      chromiumLauncher: chromiumLauncher,
709
      logger: BufferLogger.test(),
710
      platform: FakePlatform(),
711
      processManager: FakeProcessManager.any(),
712
    );
713 714
    webDevFS.report = UpdateFSReport(success: true);

715 716
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
717 718 719 720
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;
721 722
    final OperationResult result =
        await residentWebRunner.restart(fullRestart: true);
723

724
    // Ensure that generated entrypoint is generated correctly.
725
    expect(webDevFS.mainUri, isNotNull);
726 727
    final String entrypointContents =
        fileSystem.file(webDevFS.mainUri).readAsStringSync();
728 729
    expect(entrypointContents, contains('// Flutter web bootstrap script'));
    expect(entrypointContents, contains("import 'dart:ui' as ui;"));
730
    expect(entrypointContents, contains('await ui.webOnlyWarmupEngine('));
731

732
    expect(logger.statusText, contains('Restarted application in'));
733
    expect(result.code, 0);
734

735
    // ensure that analytics are sent.
736
    expect(testUsage.events, <TestUsageEvent>[
737 738 739 740 741 742 743 744 745
      TestUsageEvent('hot', 'restart',
          parameters: CustomDimensions.fromMap(<String, String>{
            'cd27': 'web-javascript',
            'cd28': '',
            'cd29': 'false',
            'cd30': 'true',
            'cd13': '0',
            'cd48': 'false'
          })),
746 747 748 749
    ]);
    expect(testUsage.timings, const <TestTimingEvent>[
      TestTimingEvent('hot', 'web-incremental-restart', Duration.zero),
    ]);
750
  }, overrides: <Type, Generator>{
751
    Usage: () => testUsage,
752 753 754
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
755

756 757
  testUsingContext('Can hot restart after attaching with web-server device',
      () async {
758 759
    final BufferLogger logger = BufferLogger.test();
    final ResidentRunner residentWebRunner = setUpResidentRunner(
760
      flutterDevice,
761
      logger: logger,
762
      systemClock: SystemClock.fixed(DateTime(2001)),
763
    );
764
    fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations);
765
    setupMocks();
766
    flutterDevice.device = webServerDevice;
767 768
    webDevFS.report = UpdateFSReport(success: true);

769 770
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
771 772 773 774
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;
775 776
    final OperationResult result =
        await residentWebRunner.restart(fullRestart: true);
777

778
    expect(logger.statusText, contains('Restarted application in'));
779
    expect(result.code, 0);
780

781
    // web-server device does not send restart analytics
782 783
    expect(testUsage.events, isEmpty);
    expect(testUsage.timings, isEmpty);
784
  }, overrides: <Type, Generator>{
785
    Usage: () => testUsage,
786 787 788
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
789

790
  testUsingContext('web resident runner is debuggable', () {
791
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
792 793
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
794

795
    expect(residentWebRunner.debuggingEnabled, true);
796 797 798 799
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
800

801
  testUsingContext('Exits when initial compile fails', () async {
802
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
803
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
804
    setupMocks();
805
    webDevFS.report = UpdateFSReport();
806

807 808
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
809
    unawaited(residentWebRunner.run(
810 811 812
      connectionInfoCompleter: connectionInfoCompleter,
    ));

813
    expect(await residentWebRunner.run(), 1);
814 815
    expect(testUsage.events, isEmpty);
    expect(testUsage.timings, isEmpty);
816
  }, overrides: <Type, Generator>{
817
    Usage: () => testUsage,
818 819 820
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
821

822 823 824
  testUsingContext(
      'Faithfully displays stdout messages with leading/trailing spaces',
      () async {
825
    final BufferLogger logger = BufferLogger.test();
826 827
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
828 829 830 831 832 833 834 835
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachLogExpectations,
      FakeVmServiceStreamResponse(
        streamId: 'Stdout',
        event: vm_service.Event(
          timestamp: 0,
          kind: vm_service.EventStreams.kStdout,
          bytes: base64.encode(
836 837
            utf8.encode(
                '    This is a message with 4 leading and trailing spaces    '),
838 839 840 841 842
          ),
        ),
      ),
      ...kAttachIsolateExpectations,
    ]);
843
    setupMocks();
844 845
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
846 847 848 849 850
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

851 852 853 854
    expect(
        logger.statusText,
        contains(
            '    This is a message with 4 leading and trailing spaces    '));
855
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
856 857 858 859
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
860

861
  testUsingContext('Fails on compilation errors in hot restart', () async {
862
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
863 864
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
865
    setupMocks();
866 867
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
868
    unawaited(residentWebRunner.run(
869 870 871
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;
872
    webDevFS.report = UpdateFSReport();
873

874 875
    final OperationResult result =
        await residentWebRunner.restart(fullRestart: true);
876 877

    expect(result.code, 1);
878
    expect(result.message, contains('Failed to recompile application.'));
879 880
    expect(testUsage.events, isEmpty);
    expect(testUsage.timings, isEmpty);
881
  }, overrides: <Type, Generator>{
882
    Usage: () => testUsage,
883 884 885
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
886

887 888 889
  testUsingContext(
      'Fails non-fatally on vmservice response error for hot restart',
      () async {
890
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
891 892 893
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
      const FakeVmServiceRequest(
894
        method: kHotRestartServiceName,
895 896
        jsonResponse: <String, Object>{
          'type': 'Failed',
897 898
        },
      ),
899
    ]);
900
    setupMocks();
901 902
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
903 904 905 906 907
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

908
    final OperationResult result = await residentWebRunner.restart();
909

910
    expect(result.code, 0);
911 912 913 914
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
915

916
  testUsingContext('Fails fatally on Vm Service error response', () async {
917
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
918 919 920
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
      const FakeVmServiceRequest(
921
        method: kHotRestartServiceName,
922 923 924 925
        // Failed response,
        errorCode: RPCErrorCodes.kInternalError,
      ),
    ]);
926
    setupMocks();
927 928
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
929 930 931 932
    unawaited(residentWebRunner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;
933
    final OperationResult result = await residentWebRunner.restart();
934 935

    expect(result.code, 1);
936
    expect(result.message, contains(RPCErrorCodes.kInternalError.toString()));
937 938 939 940
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
941

942 943
  testUsingContext('printHelp without details shows hot restart help message',
      () async {
944
    final BufferLogger logger = BufferLogger.test();
945 946
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
947 948 949
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
    residentWebRunner.printHelp(details: false);

950
    expect(logger.statusText, contains('To hot restart changes'));
951 952 953 954 955
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

956 957
  testUsingContext('cleanup of resources is safe to call multiple times',
      () async {
958
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
959
    mockDevice.dds = DartDevelopmentService();
960 961 962
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
    ]);
963
    setupMocks();
964 965
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
966
    unawaited(residentWebRunner.run(
967 968 969 970 971 972
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

    await residentWebRunner.exit();
    await residentWebRunner.exit();
973

974
    expect(debugConnection.didClose, false);
975
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
976 977 978 979
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
980

981
  testUsingContext('cleans up Chrome if tab is closed', () async {
982
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
983 984 985
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
    ]);
986
    setupMocks();
987 988
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
989
    final Future<int?> result = residentWebRunner.run(
990 991 992
      connectionInfoCompleter: connectionInfoCompleter,
    );
    await connectionInfoCompleter.future;
993
    debugConnection.completer.complete();
994 995

    await result;
996
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
997 998 999 1000
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1001

1002
  testUsingContext('Prints target and device name on run', () async {
1003
    final BufferLogger logger = BufferLogger.test();
1004 1005
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
1006 1007 1008
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachExpectations,
    ]);
1009
    setupMocks();
1010
    mockDevice.name = 'Chromez';
1011 1012
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
1013
    unawaited(residentWebRunner.run(
1014 1015 1016 1017
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

1018 1019 1020 1021 1022 1023
    expect(
        logger.statusText,
        contains(
          'Launching ${fileSystem.path.join('lib', 'main.dart')} on '
          'Chromez in debug mode',
        ));
1024
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1025 1026 1027 1028
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1029

1030 1031
  testUsingContext('Sends launched app.webLaunchUrl event for Chrome device',
      () async {
1032
    final BufferLogger logger = BufferLogger.test();
1033 1034
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      ...kAttachLogExpectations,
1035
      ...kAttachIsolateExpectations,
1036
    ]);
1037
    setupMocks();
1038
    final FakeChromeConnection chromeConnection = FakeChromeConnection();
1039
    final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
1040 1041
    final Chromium chrome =
        Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
1042
    chromiumLauncher.setInstance(chrome);
1043

1044
    flutterDevice.device = GoogleChromeDevice(
1045
      fileSystem: fileSystem,
1046
      chromiumLauncher: chromiumLauncher,
1047
      logger: logger,
1048
      platform: FakePlatform(),
1049
      processManager: FakeProcessManager.any(),
1050
    );
1051 1052
    webDevFS.baseUri = Uri.parse('http://localhost:8765/app/');

1053 1054
    final FakeChromeTab chromeTab = FakeChromeTab('index.html');
    chromeConnection.tabs.add(chromeTab);
1055

1056
    final ResidentWebRunner runner = ResidentWebRunner(
1057
      flutterDevice,
1058 1059
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
1060 1061
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
1062 1063 1064 1065 1066
      fileSystem: fileSystem,
      logger: logger,
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
1067

1068 1069
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
1070 1071 1072 1073 1074 1075
    unawaited(runner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

    // Ensure we got the URL and that it was already launched.
1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
    expect(
        logger.eventText,
        contains(json.encode(
          <String, Object>{
            'name': 'app.webLaunchUrl',
            'args': <String, Object>{
              'url': 'http://localhost:8765/app/',
              'launched': true,
            },
          },
        )));
1087
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1088
  }, overrides: <Type, Generator>{
1089 1090 1091
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1092

1093 1094 1095
  testUsingContext(
      'Sends unlaunched app.webLaunchUrl event for Web Server device',
      () async {
1096
    final BufferLogger logger = BufferLogger.test();
1097
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
1098
    setupMocks();
1099
    flutterDevice.device = WebServerDevice(
1100
      logger: logger,
1101
    );
1102
    webDevFS.baseUri = Uri.parse('http://localhost:8765/app/');
1103

1104
    final ResidentWebRunner runner = ResidentWebRunner(
1105
      flutterDevice,
1106 1107
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
1108 1109
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
1110 1111 1112 1113 1114
      fileSystem: fileSystem,
      logger: logger,
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );
1115

1116 1117
    final Completer<DebugConnectionInfo> connectionInfoCompleter =
        Completer<DebugConnectionInfo>();
1118 1119 1120 1121 1122 1123
    unawaited(runner.run(
      connectionInfoCompleter: connectionInfoCompleter,
    ));
    await connectionInfoCompleter.future;

    // Ensure we got the URL and that it was not already launched.
1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
    expect(
        logger.eventText,
        contains(json.encode(
          <String, Object>{
            'name': 'app.webLaunchUrl',
            'args': <String, Object>{
              'url': 'http://localhost:8765/app/',
              'launched': false,
            },
          },
        )));
1135
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1136
  }, overrides: <Type, Generator>{
1137 1138 1139
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1140

1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203
  testUsingContext('ResidentWebRunner generates files when l10n.yaml exists', () async {
    fakeVmServiceHost =
        FakeVmServiceHost(requests: kAttachExpectations.toList());
    setupMocks();
    final ResidentRunner residentWebRunner = ResidentWebRunner(
      flutterDevice,
      flutterProject:
          FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      ipv6: true,
      stayResident: false,
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      usage: globals.flutterUsage,
      systemClock: globals.systemClock,
    );

    // Create necessary files.
    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
    globals.fs.file(globals.fs.path.join('lib', 'l10n', 'app_en.arb'))
      ..createSync(recursive: true)
      ..writeAsStringSync('''
{
  "helloWorld": "Hello, World!",
  "@helloWorld": {
    "description": "Sample description"
  }
}''');
    globals.fs.file('l10n.yaml').createSync();
    globals.fs.file('pubspec.yaml').writeAsStringSync('''
flutter:
  generate: true
''');
    globals.fs.directory('.dart_tool')
      .childFile('package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "path_provider_linux",
      "rootUri": "../../../path_provider_linux",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ]
}
''');
    expect(await residentWebRunner.run(), 0);
    final File generatedLocalizationsFile = globals.fs.directory('.dart_tool')
      .childDirectory('flutter_gen')
      .childDirectory('gen_l10n')
      .childFile('app_localizations.dart');
    expect(generatedLocalizationsFile.existsSync(), isTrue);
    // Completing this future ensures that the daemon can exit correctly.
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

1204 1205
  // While this file should be ignored on web, generating it here will cause a
  // perf regression in hot restart.
1206
  testUsingContext('Does not generate dart_plugin_registrant.dart', () async {
1207
    // Create necessary files for [DartPluginRegistrantTarget]
1208 1209
    final File packageConfig =
        globals.fs.directory('.dart_tool').childFile('package_config.json');
1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223
    packageConfig.createSync(recursive: true);
    packageConfig.writeAsStringSync('''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "path_provider_linux",
      "rootUri": "../../../path_provider_linux",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ]
}
''');
1224
    // Start with a dart_plugin_registrant.dart file.
1225 1226 1227 1228 1229
    globals.fs
        .directory('.dart_tool')
        .childDirectory('flutter_build')
        .childFile('dart_plugin_registrant.dart')
        .createSync(recursive: true);
1230

1231 1232
    final FlutterProject project =
        FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
1233 1234 1235 1236

    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
    await residentWebRunner.runSourceGenerators();

1237
    // dart_plugin_registrant.dart should be untouched, indicating that its
1238 1239 1240 1241 1242 1243 1244 1245 1246
    // generation didn't run. If it had run, the file would have been removed as
    // there are no plugins in the project.
    expect(project.dartPluginRegistrant.existsSync(), true);
    expect(project.dartPluginRegistrant.readAsStringSync(), '');
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

1247 1248
  testUsingContext('Successfully turns WebSocketException into ToolExit',
      () async {
1249
    final BufferLogger logger = BufferLogger.test();
1250 1251
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
1252
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
1253
    setupMocks();
1254
    webDevFS.exception = const WebSocketException();
1255

1256
    await expectLater(residentWebRunner.run, throwsToolExit());
1257
    expect(logger.errorText, contains('WebSocketException'));
1258
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1259 1260 1261 1262
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1263

1264 1265
  testUsingContext('Successfully turns AppConnectionException into ToolExit',
      () async {
1266
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
1267
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
1268
    setupMocks();
1269
    webDevFS.exception = AppConnectionException('');
1270

1271
    await expectLater(residentWebRunner.run, throwsToolExit());
1272
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1273 1274 1275 1276
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1277

1278 1279
  testUsingContext('Successfully turns ChromeDebugError into ToolExit',
      () async {
1280
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
1281
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
1282
    setupMocks();
1283

1284 1285 1286
    webDevFS.exception = ChromeDebugException(<String, Object?>{
      'text': 'error',
    });
1287

1288
    await expectLater(residentWebRunner.run, throwsToolExit());
1289
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1290 1291 1292 1293
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1294

1295
  testUsingContext('Rethrows unknown Exception type from dwds', () async {
1296
    final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
1297
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
1298
    setupMocks();
1299
    webDevFS.exception = Exception();
1300

1301
    await expectLater(residentWebRunner.run, throwsException);
1302
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1303 1304 1305 1306
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1307

1308
  testUsingContext('Rethrows unknown Error type from dwds tooling', () async {
1309
    final BufferLogger logger = BufferLogger.test();
1310 1311
    final ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger);
1312
    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
1313
    setupMocks();
1314
    webDevFS.exception = StateError('');
1315

1316
    await expectLater(residentWebRunner.run, throwsStateError);
1317
    expect(fakeVmServiceHost.hasRemainingExpectations, false);
1318
  }, overrides: <Type, Generator>{
1319 1320 1321
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338

  testUsingContext('throws when port is an integer outside the valid TCP range', () async {
    final BufferLogger logger = BufferLogger.test();

    DebuggingOptions debuggingOptions = DebuggingOptions.enabled(BuildInfo.debug, port: '65536');
    ResidentRunner residentWebRunner =
        setUpResidentRunner(flutterDevice, logger: logger, debuggingOptions: debuggingOptions);
    await expectToolExitLater(residentWebRunner.run(), matches('Invalid port: 65536.*'));

    debuggingOptions = DebuggingOptions.enabled(BuildInfo.debug, port: '-1');
    residentWebRunner =
      setUpResidentRunner(flutterDevice, logger: logger, debuggingOptions: debuggingOptions);
    await expectToolExitLater(residentWebRunner.run(), matches('Invalid port: -1.*'));
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });
1339 1340
}

1341 1342
ResidentRunner setUpResidentRunner(
  FlutterDevice flutterDevice, {
1343 1344 1345
  Logger? logger,
  SystemClock? systemClock,
  DebuggingOptions? debuggingOptions,
1346 1347
}) {
  return ResidentWebRunner(
1348
    flutterDevice,
1349 1350 1351 1352
    flutterProject:
        FlutterProject.fromDirectoryTest(globals.fs.currentDirectory),
    debuggingOptions:
        debuggingOptions ?? DebuggingOptions.enabled(BuildInfo.debug),
1353
    ipv6: true,
1354 1355 1356 1357
    usage: globals.flutterUsage,
    systemClock: systemClock ?? SystemClock.fixed(DateTime.now()),
    fileSystem: globals.fs,
    logger: logger ?? BufferLogger.test(),
1358
    devtoolsHandler: createNoOpHandler,
1359
  );
1360 1361
}

1362 1363 1364
// 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
1365
class FakeWebServerDevice extends FakeDevice implements WebServerDevice {}
1366

1367 1368 1369
// 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
1370 1371
class FakeDevice extends Fake implements Device {
  @override
1372
  String name = 'FakeDevice';
1373 1374 1375 1376 1377 1378 1379

  int count = 0;

  @override
  Future<String> get sdkNameAndVersion async => 'SDK Name and Version';

  @override
1380
  late DartDevelopmentService dds;
1381 1382 1383

  @override
  Future<LaunchResult> startApp(
1384
    ApplicationPackage? package, {
1385 1386 1387 1388
    String? mainPath,
    String? route,
    DebuggingOptions? debuggingOptions,
    Map<String, dynamic>? platformArgs,
1389 1390
    bool prebuiltApplication = false,
    bool ipv6 = false,
1391
    String? userIdentifier,
1392 1393 1394 1395 1396 1397
  }) async {
    return LaunchResult.succeeded();
  }

  @override
  Future<bool> stopApp(
1398
    ApplicationPackage? app, {
1399
    String? userIdentifier,
1400 1401 1402 1403 1404 1405 1406 1407 1408 1409
  }) async {
    if (count > 0) {
      throw StateError('stopApp called more than once.');
    }
    count += 1;
    return true;
  }
}

class FakeDebugConnection extends Fake implements DebugConnection {
1410
  late FakeVmServiceHost Function() fakeVmServiceHost;
1411 1412

  @override
1413 1414
  vm_service.VmService get vmService =>
      fakeVmServiceHost.call().vmService.service;
1415 1416

  @override
1417
  late String uri;
1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438

  final Completer<void> completer = Completer<void>();
  bool didClose = false;

  @override
  Future<void> get onDone => completer.future;

  @override
  Future<void> close() async {
    didClose = true;
  }
}

class FakeAppConnection extends Fake implements AppConnection {
  bool ranMain = false;

  @override
  void runMain() {
    ranMain = true;
  }
}
1439

1440 1441 1442
// 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
1443
class FakeChromeDevice extends Fake implements ChromiumDevice {}
1444

1445
class FakeWipDebugger extends Fake implements WipDebugger {}
1446 1447 1448 1449 1450

class FakeResidentCompiler extends Fake implements ResidentCompiler {
  @override
  Future<CompilerOutput> recompile(
    Uri mainUri,
1451 1452 1453 1454 1455
    List<Uri>? invalidatedFiles, {
    required String outputPath,
    required PackageConfig packageConfig,
    required FileSystem fs,
    String? projectRootPath,
1456
    bool suppressErrors = false,
1457
    bool checkDartPluginRegistry = false,
1458
    File? dartPluginRegistrant,
1459 1460 1461 1462 1463
  }) async {
    return const CompilerOutput('foo.dill', 0, <Uri>[]);
  }

  @override
1464
  void accept() {}
1465 1466

  @override
1467
  void reset() {}
1468 1469 1470 1471 1472 1473 1474

  @override
  Future<CompilerOutput> reject() async {
    return const CompilerOutput('foo.dill', 0, <Uri>[]);
  }

  @override
1475
  void addFileSystemRoot(String root) {}
1476 1477 1478
}

class FakeWebDevFS extends Fake implements WebDevFS {
1479 1480 1481
  Object? exception;
  ConnectionResult? result;
  late UpdateFSReport report;
1482

1483
  Uri? mainUri;
1484 1485 1486 1487 1488 1489 1490 1491

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

  @override
  Uri baseUri = Uri.parse('http://localhost:12345');

  @override
1492
  DateTime? lastCompiled = DateTime.now();
1493 1494

  @override
1495
  PackageConfig? lastPackageConfig = PackageConfig.empty;
1496 1497 1498 1499 1500 1501 1502 1503

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

  @override
  Future<UpdateFSReport> update({
1504 1505 1506 1507 1508 1509 1510 1511
    required Uri mainUri,
    required ResidentCompiler generator,
    required bool trackWidgetCreation,
    required String pathToReload,
    required List<Uri> invalidatedFiles,
    required PackageConfig packageConfig,
    required String dillOutputPath,
    required DevelopmentShaderCompiler shaderCompiler,
1512
    DevelopmentSceneImporter? sceneImporter,
1513 1514 1515 1516
    DevFSWriter? devFSWriter,
    String? target,
    AssetBundle? bundle,
    DateTime? firstBuildTime,
1517 1518
    bool bundleFirstUpload = false,
    bool fullRestart = false,
1519 1520
    String? projectRootPath,
    File? dartPluginRegistrant,
1521 1522 1523 1524 1525 1526
  }) async {
    this.mainUri = mainUri;
    return report;
  }

  @override
1527
  Future<ConnectionResult?> connect(bool useDebugExtension, {VmServiceFactory vmServiceFactory = createVmServiceDelegate}) async {
1528
    if (exception != null) {
1529 1530
      assert(exception is Exception || exception is Error);
      // ignore: only_throw_errors, exception is either Error or Exception here.
1531
      throw exception!;
1532 1533 1534 1535
    }
    return result;
  }
}
1536 1537 1538 1539 1540

class FakeChromeConnection extends Fake implements ChromeConnection {
  final List<ChromeTab> tabs = <ChromeTab>[];

  @override
1541
  Future<ChromeTab> getTab(bool Function(ChromeTab tab) accept,
1542
      {Duration? retryFor}) async {
1543 1544
    return tabs.firstWhere(accept);
  }
1545 1546

  @override
1547
  Future<List<ChromeTab>> getTabs({Duration? retryFor}) async {
1548 1549
    return tabs;
  }
1550 1551 1552 1553 1554 1555 1556 1557 1558 1559
}

class FakeChromeTab extends Fake implements ChromeTab {
  FakeChromeTab(this.url);

  @override
  final String url;
  final FakeWipConnection connection = FakeWipConnection();

  @override
1560
  Future<WipConnection> connect({Function? onError}) async {
1561 1562 1563 1564 1565 1566 1567 1568 1569
    return connection;
  }
}

class FakeWipConnection extends Fake implements WipConnection {
  @override
  final WipDebugger debugger = FakeWipDebugger();
}

1570 1571 1572 1573
/// A test implementation of the [ChromiumLauncher] that launches a fixed instance.
class TestChromiumLauncher implements ChromiumLauncher {
  TestChromiumLauncher();

1574 1575
  bool _hasInstance = false;
  void setInstance(Chromium chromium) {
1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599
    _hasInstance = true;
    currentCompleter.complete(chromium);
  }

  @override
  Completer<Chromium> currentCompleter = Completer<Chromium>();

  @override
  bool canFindExecutable() {
    return true;
  }

  @override
  Future<Chromium> get connectedInstance => currentCompleter.future;

  @override
  String findExecutable() {
    return 'chrome';
  }

  @override
  bool get hasChromeInstance => _hasInstance;

  @override
1600 1601 1602
  Future<Chromium> launch(
    String url, {
    bool headless = false,
1603
    int? debugPort,
1604
    bool skipCheck = false,
1605
    Directory? cacheDir,
1606 1607
    List<String> webBrowserFlags = const <String>[],
  }) async {
1608 1609
    return currentCompleter.future;
  }
1610 1611 1612 1613 1614

  @override
  Future<Chromium> connect(Chromium chrome, bool skipCheck) {
    return currentCompleter.future;
  }
1615
}
1616 1617

class FakeFlutterDevice extends Fake implements FlutterDevice {
1618
  Uri? testUri;
1619 1620 1621 1622
  UpdateFSReport report = UpdateFSReport(
    success: true,
    invalidatedSourcesCount: 1,
  );
1623
  Exception? reportError;
1624 1625

  @override
1626
  ResidentCompiler? generator;
1627 1628

  @override
1629
  Stream<Uri?> get vmServiceUris => Stream<Uri?>.value(testUri);
1630

1631 1632 1633
  @override
  DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler();

1634
  @override
1635
  FlutterVmService? vmService;
1636

1637
  DevFS? _devFS;
1638 1639

  @override
1640
  DevFS? get devFS => _devFS;
1641 1642

  @override
1643
  set devFS(DevFS? value) {}
1644 1645

  @override
1646
  Device? device;
1647 1648

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

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

  @override
1655
  Future<Uri?> setupDevFS(String fsName, Directory rootDirectory) async {
1656 1657 1658 1659
    return testUri;
  }

  @override
1660 1661
  Future<void> exitApps(
      {Duration timeoutDelay = const Duration(seconds: 10)}) async {}
1662 1663 1664

  @override
  Future<void> connect({
1665 1666 1667 1668
    ReloadSources? reloadSources,
    Restart? restart,
    CompileExpression? compileExpression,
    GetSkSLMethod? getSkSLMethod,
1669
    FlutterProject? flutterProject,
1670 1671 1672
    PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
    int? hostVmServicePort,
    int? ddsPort,
1673 1674
    bool disableServiceAuthCodes = false,
    bool enableDds = true,
1675
    bool cacheStartupProfile = false,
1676 1677
    required bool allowExistingDdsInstance,
    bool? ipv6 = false,
1678
  }) async {}
1679 1680 1681

  @override
  Future<UpdateFSReport> updateDevFS({
1682 1683 1684 1685
    Uri? mainUri,
    String? target,
    AssetBundle? bundle,
    DateTime? firstBuildTime,
1686 1687 1688
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
1689 1690 1691 1692 1693 1694
    String? projectRootPath,
    String? pathToReload,
    String? dillOutputPath,
    List<Uri>? invalidatedFiles,
    PackageConfig? packageConfig,
    File? dartPluginRegistrant,
1695 1696
  }) async {
    if (reportError != null) {
1697
      throw reportError!;
1698 1699 1700 1701 1702
    }
    return report;
  }

  @override
1703
  Future<void> updateReloadStatus(bool wasReloadSuccessful) async {}
1704
}
1705 1706 1707 1708 1709

class FakeShaderCompiler implements DevelopmentShaderCompiler {
  const FakeShaderCompiler();

  @override
1710 1711 1712 1713
  void configureCompiler(
    TargetPlatform? platform, {
    required ImpellerStatus impellerStatus,
  }) { }
1714 1715 1716 1717 1718 1719

  @override
  Future<DevFSContent> recompileShader(DevFSContent inputShader) {
    throw UnimplementedError();
  }
}