flutter_command_test.dart 11.8 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'dart:async';
import 'dart:io' as io;

8
import 'package:flutter_tools/src/base/common.dart';
9
import 'package:flutter_tools/src/base/io.dart';
10
import 'package:flutter_tools/src/base/signals.dart';
11 12 13
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
14
import 'package:flutter_tools/src/runner/flutter_command.dart';
15
import 'package:flutter_tools/src/version.dart';
16 17
import 'package:mockito/mockito.dart';

18 19
import '../../src/common.dart';
import '../../src/context.dart';
20
import 'utils.dart';
21

22
void main() {
23
  group('Flutter Command', () {
24 25
    MockitoCache cache;
    MockitoUsage usage;
26
    MockClock clock;
27
    MockProcessInfo mockProcessInfo;
28
    List<int> mockTimes;
29 30

    setUp(() {
31 32
      cache = MockitoCache();
      usage = MockitoUsage();
33
      clock = MockClock();
34 35
      mockProcessInfo = MockProcessInfo();

36 37
      when(usage.isFirstRun).thenReturn(false);
      when(clock.now()).thenAnswer(
38
        (Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
39
      );
40
      when(mockProcessInfo.maxRss).thenReturn(10);
41 42 43
    });

    testUsingContext('honors shouldUpdateCache false', () async {
44
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: false);
45 46 47 48 49 50 51 52
      await flutterCommand.run();
      verifyZeroInteractions(cache);
    },
    overrides: <Type, Generator>{
      Cache: () => cache,
    });

    testUsingContext('honors shouldUpdateCache true', () async {
53
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true);
54
      await flutterCommand.run();
55
      verify(cache.updateAll(any)).called(1);
56 57 58 59
    },
    overrides: <Type, Generator>{
      Cache: () => cache,
    });
60

61 62 63 64 65 66 67 68 69
    void testUsingCommandContext(String testName, Function testBody) {
      testUsingContext(testName, testBody, overrides: <Type, Generator>{
        ProcessInfo: () => mockProcessInfo,
        SystemClock: () => clock,
        Usage: () => usage,
      });
    }

    testUsingCommandContext('reports command that results in success', () async {
70 71 72 73 74 75 76 77 78 79
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          return const FlutterCommandResult(ExitStatus.success);
        }
      );
      await flutterCommand.run();

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
      verify(usage.sendCommand(
        'dummy',
        parameters: anyNamed('parameters'),
      ));
      verify(usage.sendEvent(
        'tool-command-result',
        'dummy',
        label: 'success',
        parameters: anyNamed('parameters'),
      ));
      expect(verify(usage.sendEvent(
          'tool-command-max-rss',
          'dummy',
          label: 'success',
          value: captureAnyNamed('value'),
        )).captured[0],
        10,
      );
98 99
    });

100
    testUsingCommandContext('reports command that results in warning', () async {
101 102 103 104 105 106 107 108 109 110
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          return const FlutterCommandResult(ExitStatus.warning);
        }
      );
      await flutterCommand.run();

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
      verify(usage.sendCommand(
        'dummy',
        parameters: anyNamed('parameters'),
      ));
      verify(usage.sendEvent(
        'tool-command-result',
        'dummy',
        label: 'warning',
        parameters: anyNamed('parameters'),
      ));
      expect(verify(usage.sendEvent(
          'tool-command-max-rss',
          'dummy',
          label: 'warning',
          value: captureAnyNamed('value'),
        )).captured[0],
        10,
      );
129 130
    });

131
    testUsingCommandContext('reports command that results in failure', () async {
132 133 134 135 136 137 138 139 140 141 142 143
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          return const FlutterCommandResult(ExitStatus.fail);
        }
      );

      try {
        await flutterCommand.run();
      } on ToolExit {
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
        verify(usage.sendCommand(
          'dummy',
          parameters: anyNamed('parameters'),
        ));
        verify(usage.sendEvent(
          'tool-command-result',
          'dummy',
          label: 'fail',
          parameters: anyNamed('parameters'),
        ));
        expect(verify(usage.sendEvent(
            'tool-command-max-rss',
            'dummy',
            label: 'fail',
            value: captureAnyNamed('value'),
          )).captured[0],
          10,
        );
162 163 164
      }
    });

165
    testUsingCommandContext('reports command that results in error', () async {
166 167 168 169 170 171 172 173 174 175 176 177 178 179
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          throwToolExit('fail');
          return null; // unreachable
        }
      );

      try {
        await flutterCommand.run();
        fail('Mock should make this fail');
      } on ToolExit {
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
        verify(usage.sendCommand(
          'dummy',
          parameters: anyNamed('parameters'),
        ));
        verify(usage.sendEvent(
          'tool-command-result',
          'dummy',
          label: 'fail',
          parameters: anyNamed('parameters'),
        ));
        expect(verify(usage.sendEvent(
            'tool-command-max-rss',
            'dummy',
            label: 'fail',
            value: captureAnyNamed('value'),
          )).captured[0],
          10,
        );
198 199 200
      }
    });

201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
    group('signals tests', () {
      MockIoProcessSignal mockSignal;
      ProcessSignal signalUnderTest;
      StreamController<io.ProcessSignal> signalController;

      setUp(() {
        mockSignal = MockIoProcessSignal();
        signalUnderTest = ProcessSignal(mockSignal);
        signalController = StreamController<io.ProcessSignal>();
        when(mockSignal.watch()).thenAnswer((Invocation invocation) => signalController.stream);
      });

      testUsingContext('reports command that is killed', () async {
        // Crash if called a third time which is unexpected.
        mockTimes = <int>[1000, 2000];

        final Completer<void> completer = Completer<void>();
        setExitFunctionForTests((int exitCode) {
          expect(exitCode, 0);
          restoreExitFunction();
          completer.complete();
        });

        final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
          commandFunction: () async {
            final Completer<void> c = Completer<void>();
            await c.future;
            return null; // unreachable
          }
        );

        unawaited(flutterCommand.run());
        signalController.add(mockSignal);
        await completer.future;

236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
        verify(usage.sendCommand(
          'dummy',
          parameters: anyNamed('parameters'),
        ));
        verify(usage.sendEvent(
          'tool-command-result',
          'dummy',
          label: 'killed',
          parameters: anyNamed('parameters'),
        ));
        expect(verify(usage.sendEvent(
            'tool-command-max-rss',
            'dummy',
            label: 'killed',
            value: captureAnyNamed('value'),
          )).captured[0],
          10,
        );
254 255 256 257 258 259 260 261 262 263 264
      }, overrides: <Type, Generator>{
        ProcessInfo: () => mockProcessInfo,
        Signals: () => FakeSignals(
          subForSigTerm: signalUnderTest,
          exitSignals: <ProcessSignal>[signalUnderTest],
        ),
        SystemClock: () => clock,
        Usage: () => usage,
      });
    });

265
    testUsingCommandContext('report execution timing by default', () async {
266 267 268
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

269
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
270 271 272 273
      await flutterCommand.run();
      verify(clock.now()).called(2);

      expect(
274
        verify(usage.sendTiming(
275
                captureAny, captureAny, captureAny,
276
                label: captureAnyNamed('label'))).captured,
277 278 279 280
        <dynamic>[
          'flutter',
          'dummy',
          const Duration(milliseconds: 1000),
281
          null,
282
        ],
283 284 285
      );
    });

286
    testUsingCommandContext('no timing report without usagePath', () async {
287 288 289
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

290
      final DummyFlutterCommand flutterCommand =
291
          DummyFlutterCommand(noUsagePath: true);
292 293
      await flutterCommand.run();
      verify(clock.now()).called(2);
294
      verifyNever(usage.sendTiming(
295 296
                   any, any, any,
                   label: anyNamed('label')));
297
    });
298

299
    testUsingCommandContext('report additional FlutterCommandResult data', () async {
300 301 302
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

303
      final FlutterCommandResult commandResult = FlutterCommandResult(
304
        ExitStatus.success,
305
        // nulls should be cleaned up.
306
        timingLabelParts: <String> ['blah1', 'blah2', null, 'blah3'],
307
        endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500),
308 309
      );

310
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
311 312
        commandFunction: () async => commandResult
      );
313 314 315
      await flutterCommand.run();
      verify(clock.now()).called(2);
      expect(
316
        verify(usage.sendTiming(
317
                captureAny, captureAny, captureAny,
318
                label: captureAnyNamed('label'))).captured,
319
        <dynamic>[
320 321
          'flutter',
          'dummy',
322
          const Duration(milliseconds: 500), // FlutterCommandResult's end time used instead.
323
          'success-blah1-blah2-blah3',
324
        ],
325 326 327
      );
    });

328
    testUsingCommandContext('report failed execution timing too', () async {
329 330 331
      // Crash if called a third time which is unexpected.
      mockTimes = <int>[1000, 2000];

332
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
333 334 335 336 337
        commandFunction: () async {
          throwToolExit('fail');
          return null; // unreachable
        },
      );
338 339 340 341 342 343 344 345 346

      try {
        await flutterCommand.run();
        fail('Mock should make this fail');
      } on ToolExit {
        // Should have still checked time twice.
        verify(clock.now()).called(2);

        expect(
347
          verify(usage.sendTiming(
348
                  captureAny, captureAny, captureAny,
349 350 351 352 353
                  label: captureAnyNamed('label'))).captured,
          <dynamic>[
            'flutter',
            'dummy',
            const Duration(milliseconds: 1000),
354 355
            'fail',
          ],
356 357
        );
      }
358
    });
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
  });
}


class FakeCommand extends FlutterCommand {
  @override
  String get description => null;

  @override
  String get name => 'fake';

  @override
  Future<FlutterCommandResult> runCommand() async {
    return null;
  }
374
}
375 376

class MockVersion extends Mock implements FlutterVersion {}
377
class MockProcessInfo extends Mock implements ProcessInfo {}
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}

class FakeSignals implements Signals {
  FakeSignals({
    this.subForSigTerm,
    List<ProcessSignal> exitSignals,
  }) : delegate = Signals(exitSignals: exitSignals);

  final ProcessSignal subForSigTerm;
  final Signals delegate;

  @override
  Object addHandler(ProcessSignal signal, SignalHandler handler) {
    if (signal == ProcessSignal.SIGTERM) {
      return delegate.addHandler(subForSigTerm, handler);
    }
    return delegate.addHandler(signal, handler);
  }

  @override
  Future<bool> removeHandler(ProcessSignal signal, Object token) =>
    delegate.removeHandler(signal, token);

  @override
  Stream<Object> get errors => delegate.errors;
}