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

import 'dart:async';

7
import 'package:file/memory.dart';
8
import 'package:file_testing/file_testing.dart';
9
import 'package:flutter_tools/src/base/file_system.dart';
10
import 'package:flutter_tools/src/base/logger.dart';
11
import 'package:flutter_tools/src/base/os.dart';
12
import 'package:flutter_tools/src/base/platform.dart';
13
import 'package:flutter_tools/src/web/chrome.dart';
14 15
import 'package:test/fake.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
16

17 18 19
import '../src/common.dart';
import '../src/fake_process_manager.dart';
import '../src/fakes.dart';
20

21
const List<String> kChromeArgs = <String>[
22 23 24 25 26 27 28 29 30 31
  '--disable-background-timer-throttling',
  '--disable-extensions',
  '--disable-popup-blocking',
  '--bwsi',
  '--no-first-run',
  '--no-default-browser-check',
  '--disable-default-apps',
  '--disable-translate',
];

32 33 34 35 36 37
const List<String> kCodeCache = <String>[
  'Cache',
  'Code Cache',
  'GPUCache',
];

38
const String kDevtoolsStderr = '\n\nDevTools listening\n\n';
39 40

void main() {
41 42 43 44 45 46
  late FileExceptionHandler exceptionHandler;
  late ChromiumLauncher chromeLauncher;
  late FileSystem fileSystem;
  late Platform platform;
  late FakeProcessManager processManager;
  late OperatingSystemUtils operatingSystemUtils;
47 48

  setUp(() {
49
    exceptionHandler = FileExceptionHandler();
50
    operatingSystemUtils = FakeOperatingSystemUtils();
51 52
    platform = FakePlatform(operatingSystem: 'macos', environment: <String, String>{
      kChromeEnvironment: 'example_chrome',
53
    });
54
    fileSystem = MemoryFileSystem.test(opHandle: exceptionHandler.opHandle);
55
    processManager = FakeProcessManager.empty();
56
    chromeLauncher = ChromiumLauncher(
57 58 59 60
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
61
      browserFinder: findChromeExecutable,
62
      logger: BufferLogger.test(),
63
    );
64 65
  });

66
  testWithoutContext('can launch chrome and connect to the devtools', () async {
67 68
    await expectReturnsNormallyLater(
      _testLaunchChrome(
69 70 71
        '/.tmp_rand0/flutter_tools_chrome_device.rand0',
        processManager,
        chromeLauncher,
72
      )
73
    );
74 75
  });

76
  testWithoutContext('cannot have two concurrent instances of chrome', () async {
77
    await _testLaunchChrome(
78 79 80 81
      '/.tmp_rand0/flutter_tools_chrome_device.rand0',
      processManager,
      chromeLauncher,
    );
82

83 84
    await expectToolExitLater(
      _testLaunchChrome(
85 86 87 88
        '/.tmp_rand0/flutter_tools_chrome_device.rand1',
        processManager,
        chromeLauncher,
      ),
89
      contains('Only one instance of chrome can be started'),
90
    );
91 92
  });

93
  testWithoutContext('can launch new chrome after stopping a previous chrome', () async {
94
    final Chromium chrome = await _testLaunchChrome(
95 96 97 98
      '/.tmp_rand0/flutter_tools_chrome_device.rand0',
      processManager,
      chromeLauncher,
    );
99
    await chrome.close();
100

101 102
    await expectReturnsNormallyLater(
      _testLaunchChrome(
103 104 105
        '/.tmp_rand0/flutter_tools_chrome_device.rand1',
        processManager,
        chromeLauncher,
106
      )
107
    );
108
  });
109

110 111 112
  testWithoutContext('does not crash if saving profile information fails due to a file system exception.', () async {
    final BufferLogger logger = BufferLogger.test();
    chromeLauncher = ChromiumLauncher(
113
      fileSystem: fileSystem,
114 115 116 117 118 119 120 121 122 123
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      browserFinder: findChromeExecutable,
      logger: logger,
    );
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'example_chrome',
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
124
        '--remote-debugging-port=12345',
125 126 127 128 129 130 131 132 133 134 135 136
        ...kChromeArgs,
        'example_url',
      ],
      stderr: kDevtoolsStderr,
    ));

    final Chromium chrome = await chromeLauncher.launch(
      'example_url',
      skipCheck: true,
      cacheDir: fileSystem.currentDirectory,
    );

137
    // Create cache dir that the Chrome launcher will attempt to persist, and a file
138 139 140
    // that will thrown an exception when it is read.
    const String directoryPrefix = '/.tmp_rand0/flutter_tools_chrome_device.rand0/Default';
    fileSystem.directory('$directoryPrefix/Local Storage')
141
      .createSync(recursive: true);
142 143 144 145 146 147 148
    final File file = fileSystem.file('$directoryPrefix/Local Storage/foo')
      ..createSync(recursive: true);
    exceptionHandler.addError(
      file,
      FileSystemOp.read,
      const FileSystemException(),
    );
149 150 151 152 153

    await chrome.close(); // does not exit with error.
    expect(logger.errorText, contains('Failed to save Chrome preferences'));
  });

154 155
  testWithoutContext('does not crash if restoring profile information fails due to a file system exception.', () async {
    final BufferLogger logger = BufferLogger.test();
156 157 158 159 160 161 162
    final File file = fileSystem.file('/Default/foo')
      ..createSync(recursive: true);
    exceptionHandler.addError(
      file,
      FileSystemOp.read,
      const FileSystemException(),
    );
163
    chromeLauncher = ChromiumLauncher(
164
      fileSystem: fileSystem,
165 166 167 168 169 170
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      browserFinder: findChromeExecutable,
      logger: logger,
    );
171

172 173 174 175
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'example_chrome',
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
176
        '--remote-debugging-port=12345',
177 178 179 180 181 182 183 184 185 186
        ...kChromeArgs,
        'example_url',
      ],
      stderr: kDevtoolsStderr,
    ));

    fileSystem.currentDirectory.childDirectory('Default').createSync();
    final Chromium chrome = await chromeLauncher.launch(
      'example_url',
      skipCheck: true,
187
      cacheDir: fileSystem.currentDirectory,
188 189
    );

190
    // Create cache dir that the Chrome launcher will attempt to persist.
191 192 193 194 195 196 197
    fileSystem.directory('/.tmp_rand0/flutter_tools_chrome_device.rand0/Default/Local Storage')
      .createSync(recursive: true);

    await chrome.close(); // does not exit with error.
    expect(logger.errorText, contains('Failed to restore Chrome preferences'));
  });

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
  testWithoutContext('can launch Chrome on x86_64 macOS', () async {
    final OperatingSystemUtils macOSUtils = FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_x64);
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: macOSUtils,
      browserFinder: findChromeExecutable,
      logger: BufferLogger.test(),
    );

    processManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>[
          'example_chrome',
          '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
          '--remote-debugging-port=12345',
          ...kChromeArgs,
          'example_url',
        ],
        stderr: kDevtoolsStderr,
219
      ),
220 221
    ]);

222 223
    await expectReturnsNormallyLater(
      chromiumLauncher.launch(
224 225
        'example_url',
        skipCheck: true,
226
      )
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    );
  });

  testWithoutContext('can launch x86_64 Chrome on ARM macOS', () async {
    final OperatingSystemUtils macOSUtils = FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm);
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: macOSUtils,
      browserFinder: findChromeExecutable,
      logger: BufferLogger.test(),
    );

    processManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>[
          'file',
          'example_chrome',
        ],
        stdout: 'Mach-O 64-bit executable x86_64',
      ),
      const FakeCommand(
        command: <String>[
          'example_chrome',
          '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
          '--remote-debugging-port=12345',
          ...kChromeArgs,
          'example_url',
        ],
        stderr: kDevtoolsStderr,
258
      ),
259 260
    ]);

261 262
    await expectReturnsNormallyLater(
      chromiumLauncher.launch(
263 264
        'example_url',
        skipCheck: true,
265
      )
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    );
  });

  testWithoutContext('can launch ARM Chrome natively on ARM macOS when installed', () async {
    final OperatingSystemUtils macOSUtils = FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm);
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: macOSUtils,
      browserFinder: findChromeExecutable,
      logger: BufferLogger.test(),
    );

    processManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>[
          'file',
          'example_chrome',
        ],
        stdout: 'Mach-O 64-bit executable arm64',
      ),
      const FakeCommand(
        command: <String>[
          '/usr/bin/arch',
          '-arm64',
          'example_chrome',
          '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
          '--remote-debugging-port=12345',
          ...kChromeArgs,
          'example_url',
        ],
        stderr: kDevtoolsStderr,
      ),
    ]);

302 303
    await expectReturnsNormallyLater(
      chromiumLauncher.launch(
304 305
        'example_url',
        skipCheck: true,
306
      )
307 308 309
    );
  });

310
  testWithoutContext('can launch chrome with a custom debug port', () async {
311 312 313
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'example_chrome',
314
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
315
        '--remote-debugging-port=10000',
316
        ...kChromeArgs,
317 318 319 320 321
        'example_url',
      ],
      stderr: kDevtoolsStderr,
    ));

322 323
    await expectReturnsNormallyLater(
      chromeLauncher.launch(
324 325 326
        'example_url',
        skipCheck: true,
        debugPort: 10000,
327
      )
328 329
    );
  });
330

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
  testWithoutContext('can launch chrome with arbitrary flags', () async {
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'example_chrome',
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
        '--remote-debugging-port=12345',
        ...kChromeArgs,
        '--autoplay-policy=no-user-gesture-required',
        '--incognito',
        '--auto-select-desktop-capture-source="Entire screen"',
        'example_url',
      ],
      stderr: kDevtoolsStderr,
    ));

    await expectReturnsNormallyLater(chromeLauncher.launch(
      'example_url',
      skipCheck: true,
      webBrowserFlags: <String>[
        '--autoplay-policy=no-user-gesture-required',
        '--incognito',
        '--auto-select-desktop-capture-source="Entire screen"',
      ],
    ));
  });

357
  testWithoutContext('can launch chrome headless', () async {
358 359 360
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'example_chrome',
361
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
362
        '--remote-debugging-port=12345',
363
        ...kChromeArgs,
364 365 366 367 368 369 370 371 372
        '--headless',
        '--disable-gpu',
        '--no-sandbox',
        '--window-size=2400,1800',
        'example_url',
      ],
      stderr: kDevtoolsStderr,
    ));

373 374
    await expectReturnsNormallyLater(
      chromeLauncher.launch(
375 376 377
        'example_url',
        skipCheck: true,
        headless: true,
378
      )
379 380
    );
  });
381

382
  testWithoutContext('can seed chrome temp directory with existing session data, excluding Cache folder', () async {
383 384
    final Completer<void> exitCompleter = Completer<void>.sync();
    final Directory dataDir = fileSystem.directory('chrome-stuff');
385 386 387 388 389
    final File preferencesFile = dataDir
      .childDirectory('Default')
      .childFile('preferences');
    preferencesFile
      ..createSync(recursive: true)
390
      ..writeAsStringSync('"exit_type":"Crashed"');
391

392
    final Directory defaultContentDirectory = dataDir
393 394 395 396 397 398
      .childDirectory('Default')
      .childDirectory('Foo');
    defaultContentDirectory.createSync(recursive: true);
    // Create Cache directories that should be skipped
    for (final String cache in kCodeCache) {
      dataDir
399
        .childDirectory('Default')
400 401 402
        .childDirectory(cache)
        .createSync(recursive: true);
    }
403

404 405 406 407
    processManager.addCommand(FakeCommand(
      command: const <String>[
        'example_chrome',
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
408
        '--remote-debugging-port=12345',
409 410 411 412 413 414
        ...kChromeArgs,
        'example_url',
      ],
      completer: exitCompleter,
      stderr: kDevtoolsStderr,
    ));
415 416 417 418

    await chromeLauncher.launch(
      'example_url',
      skipCheck: true,
419
      cacheDir: dataDir,
420 421 422
    );

    exitCompleter.complete();
423
    await Future<void>.delayed(const Duration(milliseconds: 1));
424 425 426

    // writes non-crash back to dart_tool
    expect(preferencesFile.readAsStringSync(), '"exit_type":"Normal"');
427

428 429 430

    // validate any Default content is copied
    final Directory defaultContentDir = fileSystem
431
        .directory('.tmp_rand0/flutter_tools_chrome_device.rand0')
432
        .childDirectory('Default')
433
        .childDirectory('Foo');
434

435 436 437 438 439 440 441 442 443
    expect(defaultContentDir, exists);

    // Validate cache dirs are not copied.
    for (final String cache in kCodeCache) {
      expect(fileSystem
        .directory('.tmp_rand0/flutter_tools_chrome_device.rand0')
        .childDirectory('Default')
        .childDirectory(cache), isNot(exists));
    }
444
  });
445 446 447 448 449

  testWithoutContext('can retry launch when glibc bug happens', () async {
    const List<String> args = <String>[
      'example_chrome',
      '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
450
      '--remote-debugging-port=12345',
451 452 453 454 455 456 457 458 459 460 461 462 463 464
      ...kChromeArgs,
      '--headless',
      '--disable-gpu',
      '--no-sandbox',
      '--window-size=2400,1800',
      'example_url',
    ];

    // Pretend to hit glibc bug 3 times.
    for (int i = 0; i < 3; i++) {
      processManager.addCommand(const FakeCommand(
        command: args,
        stderr: 'Inconsistency detected by ld.so: ../elf/dl-tls.c: 493: '
                '_dl_allocate_tls_init: Assertion `listp->slotinfo[cnt].gen '
465
                "<= GL(dl_tls_generation)' failed!",
466 467 468 469 470 471 472 473 474
      ));
    }

    // Succeed on the 4th try.
    processManager.addCommand(const FakeCommand(
      command: args,
      stderr: kDevtoolsStderr,
    ));

475 476
    await expectReturnsNormallyLater(
      chromeLauncher.launch(
477 478 479
        'example_url',
        skipCheck: true,
        headless: true,
480
      )
481 482 483
    );
  });

484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
  testWithoutContext('can retry launch when chrome fails to start', () async {
    const List<String> args = <String>[
      'example_chrome',
      '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
      '--remote-debugging-port=12345',
      ...kChromeArgs,
      '--headless',
      '--disable-gpu',
      '--no-sandbox',
      '--window-size=2400,1800',
      'example_url',
    ];

    // Pretend to random error 3 times.
    for (int i = 0; i < 3; i++) {
      processManager.addCommand(const FakeCommand(
        command: args,
        stderr: 'BLAH BLAH',
      ));
    }

    // Succeed on the 4th try.
506
    processManager.addCommand(const FakeCommand(
507 508
      command: args,
      stderr: kDevtoolsStderr,
509 510
    ));

511 512
    await expectReturnsNormallyLater(
      chromeLauncher.launch(
513 514 515
        'example_url',
        skipCheck: true,
        headless: true,
516
      )
517 518 519 520
    );
  });

  testWithoutContext('gives up retrying when an error happens more than 3 times', () async {
521 522 523 524 525 526 527 528 529
    final BufferLogger logger = BufferLogger.test();
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      browserFinder: findChromeExecutable,
      logger: logger,
    );
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
    for (int i = 0; i < 4; i++) {
      processManager.addCommand(const FakeCommand(
        command: <String>[
          'example_chrome',
          '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
          '--remote-debugging-port=12345',
          ...kChromeArgs,
          '--headless',
          '--disable-gpu',
          '--no-sandbox',
          '--window-size=2400,1800',
          'example_url',
        ],
        stderr: 'nothing in the std error indicating glibc error',
      ));
    }

547
    await expectToolExitLater(
548
      chromiumLauncher.launch(
549 550 551 552
        'example_url',
        skipCheck: true,
        headless: true,
      ),
553
      contains('Failed to launch browser.'),
554
    );
555
    expect(logger.errorText, contains('nothing in the std error indicating glibc error'));
556
  });
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585

  testWithoutContext('Logs an error and exits if connection check fails.', () async {
    final BufferLogger logger = BufferLogger.test();
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      browserFinder: findChromeExecutable,
      logger: logger,
    );
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'example_chrome',
        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
        '--remote-debugging-port=12345',
        ...kChromeArgs,
        'example_url',
      ],
      stderr: kDevtoolsStderr,
    ));

    await expectToolExitLater(
      chromiumLauncher.launch(
        'example_url',
      ),
      contains('Unable to connect to Chrome debug port:'),
    );
    expect(logger.errorText, contains('SocketException'));
586
  });
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628

  test('can recover if getTabs throws a connection exception', () async {
    final BufferLogger logger = BufferLogger.test();
    final FakeChromeConnection chromeConnection = FakeChromeConnection(maxRetries: 4);
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      browserFinder: findChromeExecutable,
      logger: logger,
    );
    final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher);
    expect(await chromiumLauncher.connect(chrome, false), equals(chrome));
    expect(logger.errorText, isEmpty);
  });

  test('exits if getTabs throws a connection exception consistently', () async {
    final BufferLogger logger = BufferLogger.test();
    final FakeChromeConnection chromeConnection = FakeChromeConnection();
    final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      browserFinder: findChromeExecutable,
      logger: logger,
    );
    final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher);
    await expectToolExitLater(
      chromiumLauncher.connect(chrome, false),
        allOf(
          contains('Unable to connect to Chrome debug port'),
          contains('incorrect format'),
        ));
    expect(logger.errorText,
      allOf(
          contains('incorrect format'),
          contains('OK'),
          contains('<html> ...'),
        ));
  });
629 630
}

631
Future<Chromium> _testLaunchChrome(String userDataDir, FakeProcessManager processManager, ChromiumLauncher chromeLauncher) {
632 633 634 635
  processManager.addCommand(FakeCommand(
    command: <String>[
      'example_chrome',
      '--user-data-dir=$userDataDir',
636
      '--remote-debugging-port=12345',
637
      ...kChromeArgs,
638 639 640 641 642 643 644 645 646 647
      'example_url',
    ],
    stderr: kDevtoolsStderr,
  ));

  return chromeLauncher.launch(
    'example_url',
    skipCheck: true,
  );
}
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680

/// Fake chrome connection that fails to get tabs a few times.
class FakeChromeConnection extends Fake implements ChromeConnection {

  /// Create a connection that throws a connection exception on first
  /// [maxRetries] calls to [getTabs].
  /// If [maxRetries] is `null`, [getTabs] calls never succeed.
  FakeChromeConnection({this.maxRetries}): _retries = 0;

  final List<ChromeTab> tabs = <ChromeTab>[];
  final int? maxRetries;
  int _retries;

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

  @override
  Future<List<ChromeTab>> getTabs({Duration? retryFor}) async {
    _retries ++;
    if (maxRetries == null || _retries < maxRetries!) {
      throw ConnectionException(
        formatException: const FormatException('incorrect format'),
        responseStatus: 'OK,',
        responseBody: '<html> ...');
    }
    return tabs;
  }

  @override
  void close() {}
}