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

5 6
// @dart = 2.8

7
import 'package:file/memory.dart';
8
import 'package:flutter_tools/src/application_package.dart';
9
import 'package:flutter_tools/src/artifacts.dart';
10
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:flutter_tools/src/base/platform.dart';
13
import 'package:flutter_tools/src/build_info.dart';
14
import 'package:flutter_tools/src/compile.dart';
15
import 'package:flutter_tools/src/devfs.dart';
16
import 'package:flutter_tools/src/device.dart';
17
import 'package:flutter_tools/src/resident_devtools_handler.dart';
18
import 'package:flutter_tools/src/resident_runner.dart';
19
import 'package:flutter_tools/src/run_hot.dart';
20
import 'package:flutter_tools/src/vmservice.dart';
21
import 'package:meta/meta.dart';
22
import 'package:test/fake.dart';
23
import 'package:vm_service/vm_service.dart' as vm_service;
24

25 26
import '../src/common.dart';
import '../src/context.dart';
27
import '../src/fake_vm_services.dart';
28

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kResume,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[],
  exceptionPauseMode: null,
  libraries: <vm_service.LibraryRef>[],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
44
  isSystemIsolate: false,
45
  isolateFlags: <vm_service.IsolateFlag>[],
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
);

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

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

62
void main() {
63 64
  group('validateReloadReport', () {
    testUsingContext('invalid', () async {
65
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
66 67 68
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{},
69 70
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
71 72 73 74 75 76
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
          ],
        },
77 78
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
79 80 81 82 83 84 85
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <String, dynamic>{
            'message': 'error',
          },
        },
86 87
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
88 89 90 91 92
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[],
        },
93 94
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
95 96 97 98
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
99
            <String, dynamic>{'message': false},
100 101
          ],
        },
102 103
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
104 105 106 107
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
108
            <String, dynamic>{'message': <String>['error']},
109 110
          ],
        },
111 112
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
113 114 115 116
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
117 118
            <String, dynamic>{'message': 'error'},
            <String, dynamic>{'message': <String>['error']},
119 120
          ],
        },
121 122
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
123 124 125 126
        'type': 'ReloadReport',
        'success': false,
        'details': <String, dynamic>{
          'notices': <Map<String, dynamic>>[
127
            <String, dynamic>{'message': 'error'},
128 129
          ],
        },
130 131
      })), false);
      expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
132 133
        'type': 'ReloadReport',
        'success': true,
134 135 136 137 138 139 140 141 142
      })), true);
    });

    testWithoutContext('ReasonForCancelling toString has a hint for specific errors', () {
      final ReasonForCancelling reasonForCancelling = ReasonForCancelling(
        message: 'Const class cannot remove fields',
      );

      expect(reasonForCancelling.toString(), contains('Try performing a hot restart instead.'));
143 144
    });
  });
145 146

  group('hotRestart', () {
147
    final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
148
    FileSystem fileSystem;
149 150

    setUp(() {
151
      fileSystem = MemoryFileSystem.test();
152 153 154
    });

    testUsingContext('setup function fails', () async {
155
      fileSystem.file('.packages')
156 157
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
158
      final FakeDevice device = FakeDevice();
159
      final List<FlutterDevice> devices = <FlutterDevice>[
160
        FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug)..devFS = FakeDevFs(),
161
      ];
162 163 164
      final OperationResult result = await HotRunner(
        devices,
        debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
165
        target: 'main.dart',
166
        devtoolsHandler: createNoOpHandler,
167
      ).restart(fullRestart: true);
168 169
      expect(result.isOk, false);
      expect(result.message, 'setupHotRestart failed');
170
    }, overrides: <Type, Generator>{
171
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: false),
172 173 174 175
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
176
    });
177

178 179 180 181 182 183 184 185 186 187
    group('shutdown hook tests', () {
      TestHotRunnerConfig shutdownTestingConfig;

      setUp(() {
        shutdownTestingConfig = TestHotRunnerConfig(
          successfulSetup: true,
        );
      });

      testUsingContext('shutdown hook called after signal', () async {
188
        fileSystem.file('.packages')
189 190
          ..createSync(recursive: true)
          ..writeAsStringSync('\n');
191
        final FakeDevice device = FakeDevice();
192
        final List<FlutterDevice> devices = <FlutterDevice>[
193
          FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug),
194
        ];
195 196
        await HotRunner(
          devices,
197 198
          debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
          target: 'main.dart',
199
        ).cleanupAfterSignal();
200
        expect(shutdownTestingConfig.shutdownHookCalled, true);
201
      }, overrides: <Type, Generator>{
202
        HotRunnerConfig: () => shutdownTestingConfig,
203 204 205 206
        Artifacts: () => Artifacts.test(),
        FileSystem: () => fileSystem,
        Platform: () => FakePlatform(operatingSystem: 'linux'),
        ProcessManager: () => FakeProcessManager.any(),
207 208 209
      });

      testUsingContext('shutdown hook called after app stop', () async {
210
        fileSystem.file('.packages')
211 212
          ..createSync(recursive: true)
          ..writeAsStringSync('\n');
213
        final FakeDevice device = FakeDevice();
214
        final List<FlutterDevice> devices = <FlutterDevice>[
215
          FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug),
216
        ];
217 218
        await HotRunner(
          devices,
219 220
          debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
          target: 'main.dart',
221
        ).preExit();
222
        expect(shutdownTestingConfig.shutdownHookCalled, true);
223
      }, overrides: <Type, Generator>{
224
        HotRunnerConfig: () => shutdownTestingConfig,
225 226 227 228
        Artifacts: () => Artifacts.test(),
        FileSystem: () => fileSystem,
        Platform: () => FakePlatform(operatingSystem: 'linux'),
        ProcessManager: () => FakeProcessManager.any(),
229 230
      });
    });
231
  });
232 233

  group('hot attach', () {
234
    FileSystem fileSystem;
235 236

    setUp(() {
237
      fileSystem = MemoryFileSystem.test();
238 239
    });

nt4f04uNd's avatar
nt4f04uNd committed
240
    testUsingContext('Exits with code 2 when HttpException is thrown '
241
      'during VM service connection', () async {
242
      fileSystem.file('.packages')
243 244 245
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');

246
      final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
247
      final FakeDevice device = FakeDevice();
248 249
      final List<FlutterDevice> devices = <FlutterDevice>[
        TestFlutterDevice(
250
          device: device,
251 252
          generator: residentCompiler,
          exception: const HttpException('Connection closed before full header was received, '
253
              'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws'),
254 255 256 257 258
        ),
      ];

      final int exitCode = await HotRunner(devices,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
259
        target: 'main.dart',
260 261 262
      ).attach(
        enableDevTools: false,
      );
263 264 265
      expect(exitCode, 2);
    }, overrides: <Type, Generator>{
      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
266 267 268 269
      Artifacts: () => Artifacts.test(),
      FileSystem: () => fileSystem,
      Platform: () => FakePlatform(operatingSystem: 'linux'),
      ProcessManager: () => FakeProcessManager.any(),
270 271
    });
  });
272 273 274

  group('hot cleanupAtFinish()', () {
    testUsingContext('disposes each device', () async {
275 276 277 278
      final FakeDevice device1 = FakeDevice();
      final FakeDevice device2 = FakeDevice();
      final FakeFlutterDevice flutterDevice1 = FakeFlutterDevice(device1);
      final FakeFlutterDevice flutterDevice2 = FakeFlutterDevice(device2);
279 280

      final List<FlutterDevice> devices = <FlutterDevice>[
281 282
        flutterDevice1,
        flutterDevice2,
283 284 285 286
      ];

      await HotRunner(devices,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
287
        target: 'main.dart',
288 289
      ).cleanupAtFinish();

290 291 292 293 294
      expect(device1.disposed, true);
      expect(device2.disposed, true);

      expect(flutterDevice1.stoppedEchoingDeviceLog, true);
      expect(flutterDevice2.stoppedEchoingDeviceLog, true);
295 296
    });
  });
297 298
}

299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
class FakeDevFs extends Fake implements DevFS {
  @override
  Future<void> destroy() async { }
}

class FakeDevice extends Fake implements Device {
  bool disposed = false;

  @override
  bool isSupported() => true;

  @override
  bool supportsHotReload = true;

  @override
  bool supportsHotRestart = true;

  @override
  bool supportsFlutterExit = true;

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester;
321

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
  @override
  Future<String> get sdkNameAndVersion async => 'Tester';

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

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

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

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
class FakeFlutterDevice extends Fake implements FlutterDevice {
  FakeFlutterDevice(this.device);

  bool stoppedEchoingDeviceLog = false;

  @override
  final FakeDevice device;

  @override
  Future<void> stopEchoingDeviceLog() async {
    stoppedEchoingDeviceLog = true;
  }

  @override
  DevFS devFS = FakeDevFs();
}
358

359 360 361 362
class TestFlutterDevice extends FlutterDevice {
  TestFlutterDevice({
    @required Device device,
    @required this.exception,
363
    @required ResidentCompiler generator,
364
  })  : assert(exception != null),
365
        super(device, buildInfo: BuildInfo.debug, generator: generator);
366 367 368 369 370 371 372 373 374

  /// The exception to throw when the connect method is called.
  final Exception exception;

  @override
  Future<void> connect({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
375
    GetSkSLMethod getSkSLMethod,
376
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
377
    bool disableServiceAuthCodes = false,
378
    bool enableDds = true,
379
    bool ipv6 = false,
380 381
    int hostVmServicePort,
    int ddsPort,
382
    bool allowExistingDdsInstance = false,
383 384 385 386 387
  }) async {
    throw exception;
  }
}

388
class TestHotRunnerConfig extends HotRunnerConfig {
389
  TestHotRunnerConfig({@required this.successfulSetup});
390
  bool successfulSetup;
391
  bool shutdownHookCalled = false;
392

393 394 395 396
  @override
  Future<bool> setupHotRestart() async {
    return successfulSetup;
  }
397 398 399 400 401

  @override
  Future<void> runPreShutdownOperations() async {
    shutdownHookCalled = true;
  }
402
}
403 404 405 406 407

class FakeResidentCompiler extends Fake implements ResidentCompiler {
  @override
  void accept() {}
}