| // 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. | |
| import 'dart:async'; | |
| import 'dart:io' as io; | |
| import 'package:flutter_devicelab/framework/browser.dart'; | |
| import '../common.dart'; | |
| import 'browser_test_json_samples.dart'; | |
| void main() { | |
| group('BlinkTraceEvent works with Chrome 89+', () { | |
| // Used to test 'false' results | |
| final unrelatedPhX = BlinkTraceEvent.fromJson(unrelatedPhXJson); | |
| final anotherUnrelated = BlinkTraceEvent.fromJson(anotherUnrelatedJson); | |
| test('isBeginFrame', () { | |
| final event = BlinkTraceEvent.fromJson(beginMainFrameJson_89plus); | |
| expect(event.isBeginFrame, isTrue); | |
| expect(unrelatedPhX.isBeginFrame, isFalse); | |
| expect(anotherUnrelated.isBeginFrame, isFalse); | |
| }); | |
| test('isUpdateAllLifecyclePhases', () { | |
| final event = BlinkTraceEvent.fromJson(updateLifecycleJson_89plus); | |
| expect(event.isUpdateAllLifecyclePhases, isTrue); | |
| expect(unrelatedPhX.isUpdateAllLifecyclePhases, isFalse); | |
| expect(anotherUnrelated.isUpdateAllLifecyclePhases, isFalse); | |
| }); | |
| test('isBeginMeasuredFrame', () { | |
| final event = BlinkTraceEvent.fromJson(beginMeasuredFrameJson_89plus); | |
| expect(event.isBeginMeasuredFrame, isTrue); | |
| expect(unrelatedPhX.isBeginMeasuredFrame, isFalse); | |
| expect(anotherUnrelated.isBeginMeasuredFrame, isFalse); | |
| }); | |
| test('isEndMeasuredFrame', () { | |
| final event = BlinkTraceEvent.fromJson(endMeasuredFrameJson_89plus); | |
| expect(event.isEndMeasuredFrame, isTrue); | |
| expect(unrelatedPhX.isEndMeasuredFrame, isFalse); | |
| expect(anotherUnrelated.isEndMeasuredFrame, isFalse); | |
| }); | |
| }); | |
| group('Chrome.connect lifecycle', () { | |
| test( | |
| 'does not report premature exit when wrapped process exits cleanly with exitCode 0', | |
| () async { | |
| final fakeProcess = FakeProcess(); | |
| final errors = <String>[]; | |
| await Chrome.connect( | |
| fakeProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors.add(error); | |
| }, | |
| ); | |
| fakeProcess.completeExit(0); | |
| await pumpEventQueue(); | |
| expect(errors, isEmpty); | |
| }, | |
| ); | |
| test('does not throw Future already completed when wrapped process terminates after completer completes', () async { | |
| final fakeProcess = FakeProcess(); | |
| final profileData = Completer<List<Map<String, dynamic>>>(); | |
| await Chrome.connect( | |
| fakeProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| profileData.completeError(Exception(error)); | |
| }, | |
| ); | |
| // Simulate benchmark completion | |
| profileData.complete(<Map<String, dynamic>>[]); | |
| await profileData.future; | |
| // Simulate graceful shutdown of the wrapped process (e.g. sending 'q' to flutter run) | |
| fakeProcess.completeExit(0); | |
| await pumpEventQueue(); | |
| }); | |
| test('reports premature exit when wrapped process crashes with non-zero exitCode', () async { | |
| final fakeProcess = FakeProcess(); | |
| final errors = <String>[]; | |
| await Chrome.connect( | |
| fakeProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors.add(error); | |
| }, | |
| ); | |
| fakeProcess.completeExit(1); | |
| await pumpEventQueue(); | |
| expect(errors, <String>['Chrome process exited prematurely with exit code 1']); | |
| }); | |
| test( | |
| 'does not report premature exit after disconnect even if wrapped process crashes', | |
| () async { | |
| final fakeProcess = FakeProcess(); | |
| final errors = <String>[]; | |
| final Chrome chrome = await Chrome.connect( | |
| fakeProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors.add(error); | |
| }, | |
| ); | |
| chrome.disconnect(); | |
| fakeProcess.completeExit(1); | |
| await pumpEventQueue(); | |
| expect(errors, isEmpty); | |
| expect(fakeProcess.wasKilled, isFalse); | |
| }, | |
| ); | |
| }); | |
| group('Chrome lifecycle edge cases', () { | |
| test( | |
| 're-entrant and redundant lifecycle calls do not throw and kill process on stop', | |
| () async { | |
| final fakeProcess = FakeProcess(); | |
| final errors = <String>[]; | |
| final Chrome chrome = await Chrome.connect( | |
| fakeProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors.add(error); | |
| }, | |
| ); | |
| // Multiple disconnect calls sequentially should not throw | |
| expect(() { | |
| chrome.disconnect(); | |
| chrome.disconnect(); | |
| chrome.disconnect(); | |
| }, returnsNormally); | |
| expect(fakeProcess.wasKilled, isFalse); | |
| // Calling stop after disconnect should not throw and should kill underlying process | |
| expect(() { | |
| chrome.stop(); | |
| chrome.stop(); | |
| }, returnsNormally); | |
| expect(fakeProcess.wasKilled, isTrue); | |
| // Redundant disconnect after stop should not throw | |
| expect(() { | |
| chrome.disconnect(); | |
| }, returnsNormally); | |
| await pumpEventQueue(); | |
| expect(errors, isEmpty); | |
| }, | |
| ); | |
| test( | |
| 'pre-exited process boundary handles exitCode 0 without error and exitCode 1 with error', | |
| () async { | |
| // 1. Process that has already exited with exitCode 0 before connection | |
| final cleanProcess = FakeProcess()..completeExit(0); | |
| final cleanErrors = <String>[]; | |
| await Chrome.connect( | |
| cleanProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| cleanErrors.add(error); | |
| }, | |
| ); | |
| await pumpEventQueue(); | |
| expect(cleanErrors, isEmpty); | |
| // 2. Process that has already exited with exitCode 1 before connection | |
| final crashedProcess = FakeProcess()..completeExit(1); | |
| final crashErrors = <String>[]; | |
| await Chrome.connect( | |
| crashedProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| crashErrors.add(error); | |
| }, | |
| ); | |
| await pumpEventQueue(); | |
| expect(crashErrors, <String>['Chrome process exited prematurely with exit code 1']); | |
| // 3. Process that completes exitCode 0 immediately upon connection | |
| final immediateCleanProcess = FakeProcess(); | |
| final immediateCleanErrors = <String>[]; | |
| final Chrome immediateCleanChrome = await Chrome.connect( | |
| immediateCleanProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| immediateCleanErrors.add(error); | |
| }, | |
| ); | |
| immediateCleanProcess.completeExit(0); | |
| await pumpEventQueue(); | |
| expect(immediateCleanErrors, isEmpty); | |
| immediateCleanChrome.disconnect(); | |
| // 4. Process that completes exitCode 1 immediately upon connection | |
| final immediateCrashProcess = FakeProcess(); | |
| final immediateCrashErrors = <String>[]; | |
| await Chrome.connect( | |
| immediateCrashProcess, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| immediateCrashErrors.add(error); | |
| }, | |
| ); | |
| immediateCrashProcess.completeExit(1); | |
| await pumpEventQueue(); | |
| expect(immediateCrashErrors, <String>[ | |
| 'Chrome process exited prematurely with exit code 1', | |
| ]); | |
| }, | |
| ); | |
| test('handles rapid disposal and immediate process exit in the same microtask turn', () async { | |
| // 1. Process exits in the same microtask turn as disconnect() | |
| final process1 = FakeProcess(); | |
| final errors1 = <String>[]; | |
| final Chrome chrome1 = await Chrome.connect( | |
| process1, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors1.add(error); | |
| }, | |
| ); | |
| // Exit and disconnect within the same synchronous turn | |
| process1.completeExit(1); | |
| chrome1.disconnect(); | |
| await pumpEventQueue(); | |
| expect(errors1, isEmpty); | |
| expect(process1.wasKilled, isFalse); | |
| // 2. Disconnect called first, then process exits in the same synchronous turn | |
| final process2 = FakeProcess(); | |
| final errors2 = <String>[]; | |
| final Chrome chrome2 = await Chrome.connect( | |
| process2, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors2.add(error); | |
| }, | |
| ); | |
| chrome2.disconnect(); | |
| process2.completeExit(1); | |
| await pumpEventQueue(); | |
| expect(errors2, isEmpty); | |
| expect(process2.wasKilled, isFalse); | |
| // 3. Process exits in the same microtask turn as stop() | |
| final process3 = FakeProcess(); | |
| final errors3 = <String>[]; | |
| final Chrome chrome3 = await Chrome.connect( | |
| process3, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors3.add(error); | |
| }, | |
| ); | |
| // stop() calls disconnect() and kills process in the same turn | |
| chrome3.stop(); | |
| await pumpEventQueue(); | |
| expect(errors3, isEmpty); | |
| expect(process3.wasKilled, isTrue); | |
| // 4. Process exits with non-zero exit code immediately before stop() in same turn | |
| final process4 = FakeProcess(); | |
| final errors4 = <String>[]; | |
| final Chrome chrome4 = await Chrome.connect( | |
| process4, | |
| ChromeOptions(), | |
| onError: (String error) { | |
| errors4.add(error); | |
| }, | |
| ); | |
| process4.completeExit(1); | |
| chrome4.stop(); | |
| await pumpEventQueue(); | |
| expect(errors4, isEmpty); | |
| expect(process4.wasKilled, isTrue); | |
| }); | |
| }); | |
| } | |
| class FakeProcess implements io.Process { | |
| final Completer<int> _exitCodeCompleter = Completer<int>(); | |
| bool wasKilled = false; | |
| @override | |
| Future<int> get exitCode => _exitCodeCompleter.future; | |
| @override | |
| bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) { | |
| wasKilled = true; | |
| if (!_exitCodeCompleter.isCompleted) { | |
| _exitCodeCompleter.complete(-1); | |
| } | |
| return true; | |
| } | |
| void completeExit(int code) { | |
| if (!_exitCodeCompleter.isCompleted) { | |
| _exitCodeCompleter.complete(code); | |
| } | |
| } | |
| @override | |
| int get pid => 12345; | |
| @override | |
| Stream<List<int>> get stderr => const Stream<List<int>>.empty(); | |
| @override | |
| Stream<List<int>> get stdout => const Stream<List<int>>.empty(); | |
| @override | |
| io.IOSink get stdin => throw UnimplementedError(); | |
| } |