flutter_command_test.dart 16.5 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 7
import 'dart:async';
import 'dart:io' as io;

8
import 'package:args/command_runner.dart';
9
import 'package:flutter_tools/src/base/common.dart';
10
import 'package:flutter_tools/src/base/error_handling_file_system.dart';
11
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:flutter_tools/src/base/signals.dart';
13 14 15
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
16
import 'package:flutter_tools/src/runner/flutter_command.dart';
17
import 'package:flutter_tools/src/version.dart';
18
import 'package:flutter_tools/src/globals.dart' as globals;
19 20
import 'package:mockito/mockito.dart';

21 22
import '../../src/common.dart';
import '../../src/context.dart';
23
import 'utils.dart';
24

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

    setUp(() {
34
      Cache.disableLocking();
35 36
      cache = MockitoCache();
      usage = MockitoUsage();
37
      clock = MockClock();
38 39
      mockProcessInfo = MockProcessInfo();

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

47 48 49 50
    tearDown(() {
      Cache.enableLocking();
    });

51
    testUsingContext('help text contains global options', () {
52
      final FakeDeprecatedCommand fake = FakeDeprecatedCommand();
53 54 55 56
      createTestCommandRunner(fake);
      expect(fake.usage, contains('Global options:\n'));
    });

57
    testUsingContext('honors shouldUpdateCache false', () async {
58
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: false);
59 60
      await flutterCommand.run();
      verifyZeroInteractions(cache);
61 62
      expect(flutterCommand.deprecated, isFalse);
      expect(flutterCommand.hidden, isFalse);
63 64 65 66 67 68
    },
    overrides: <Type, Generator>{
      Cache: () => cache,
    });

    testUsingContext('honors shouldUpdateCache true', () async {
69
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true);
70
      await flutterCommand.run();
71 72 73 74 75 76 77 78
      // First call for universal, second for the rest
      expect(
        verify(cache.updateAll(captureAny)).captured,
        <Set<DevelopmentArtifact>>[
          <DevelopmentArtifact>{DevelopmentArtifact.universal},
          <DevelopmentArtifact>{},
        ],
      );
79 80 81 82
    },
    overrides: <Type, Generator>{
      Cache: () => cache,
    });
83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    testUsingContext('deprecated command should warn', () async {
      final FakeDeprecatedCommand flutterCommand = FakeDeprecatedCommand();
      final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
      await runner.run(<String>['deprecated']);

      expect(testLogger.statusText,
        contains('The "deprecated" command is deprecated and will be removed in '
            'a future version of Flutter.'));
      expect(flutterCommand.usage,
        contains('Deprecated. This command will be removed in a future version '
            'of Flutter.'));
      expect(flutterCommand.deprecated, isTrue);
      expect(flutterCommand.hidden, isTrue);
    });

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
    testUsingContext('null-safety is surfaced in command usage analytics', () async {
      final FakeNullSafeCommand fake = FakeNullSafeCommand();
      final CommandRunner<void> commandRunner = createTestCommandRunner(fake);

      await commandRunner.run(<String>['safety', '--enable-experiment=non-nullable']);

      final VerificationResult resultA = verify(usage.sendCommand(
        'safety',
        parameters: captureAnyNamed('parameters'),
      ));
      expect(resultA.captured.first, containsPair('cd47', 'true'));
      reset(usage);

      await commandRunner.run(<String>['safety', '--enable-experiment=foo']);

      final VerificationResult resultB = verify(usage.sendCommand(
        'safety',
        parameters: captureAnyNamed('parameters'),
      ));
      expect(resultB.captured.first, containsPair('cd47', 'false'));
    }, overrides: <Type, Generator>{
      Usage: () => usage,
    });

123 124 125
    testUsingContext('uses the error handling file system', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
126
          expect(globals.fs, isA<ErrorHandlingFileSystem>());
127 128 129 130 131 132
          return const FlutterCommandResult(ExitStatus.success);
        }
      );
      await flutterCommand.run();
    });

133
    void testUsingCommandContext(String testName, dynamic Function() testBody) {
134 135 136 137 138 139 140 141
      testUsingContext(testName, testBody, overrides: <Type, Generator>{
        ProcessInfo: () => mockProcessInfo,
        SystemClock: () => clock,
        Usage: () => usage,
      });
    }

    testUsingCommandContext('reports command that results in success', () async {
142 143 144 145 146 147 148 149 150 151
      // 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();

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
      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,
      );
170 171
    });

172
    testUsingCommandContext('reports command that results in warning', () async {
173 174 175 176 177 178 179 180 181 182
      // 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();

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
      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,
      );
201 202
    });

203
    testUsingCommandContext('reports command that results in failure', () async {
204 205 206 207 208 209 210 211 212 213 214 215
      // 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 {
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        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,
        );
234 235 236
      }
    });

237
    testUsingCommandContext('reports command that results in error', () async {
238 239 240 241 242 243 244 245 246 247 248 249 250 251
      // 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 {
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        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,
        );
270 271 272
      }
    });

273 274 275 276 277 278 279 280
    test('FlutterCommandResult.success()', () async {
      expect(FlutterCommandResult.success().exitStatus, ExitStatus.success);
    });

    test('FlutterCommandResult.warning()', () async {
      expect(FlutterCommandResult.warning().exitStatus, ExitStatus.warning);
    });

281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
    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;

316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
        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,
        );
334 335 336 337 338 339 340 341 342
      }, overrides: <Type, Generator>{
        ProcessInfo: () => mockProcessInfo,
        Signals: () => FakeSignals(
          subForSigTerm: signalUnderTest,
          exitSignals: <ProcessSignal>[signalUnderTest],
        ),
        SystemClock: () => clock,
        Usage: () => usage,
      });
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370

      testUsingContext('command release lock on kill signal', () async {
        mockTimes = <int>[1000, 2000];
        final Completer<void> completer = Completer<void>();
        setExitFunctionForTests((int exitCode) {
          expect(exitCode, 0);
          restoreExitFunction();
          completer.complete();
        });
        final Completer<void> checkLockCompleter = Completer<void>();
        final DummyFlutterCommand flutterCommand =
            DummyFlutterCommand(commandFunction: () async {
          await Cache.lock();
          checkLockCompleter.complete();
          final Completer<void> c = Completer<void>();
          await c.future;
          return null; // unreachable
        });

        unawaited(flutterCommand.run());
        await checkLockCompleter.future;

        Cache.checkLockAcquired();

        signalController.add(mockSignal);
        await completer.future;

        await Cache.lock();
371
        Cache.releaseLock();
372 373 374 375 376 377 378 379
      }, overrides: <Type, Generator>{
        ProcessInfo: () => mockProcessInfo,
        Signals: () => FakeSignals(
              subForSigTerm: signalUnderTest,
              exitSignals: <ProcessSignal>[signalUnderTest],
            ),
        Usage: () => usage
      });
380 381
    });

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

386
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
387 388 389 390
      await flutterCommand.run();
      verify(clock.now()).called(2);

      expect(
391
        verify(usage.sendTiming(
392
                captureAny, captureAny, captureAny,
393
                label: captureAnyNamed('label'))).captured,
394 395 396 397
        <dynamic>[
          'flutter',
          'dummy',
          const Duration(milliseconds: 1000),
398
          'fail',
399
        ],
400 401 402
      );
    });

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

407
      final DummyFlutterCommand flutterCommand =
408
          DummyFlutterCommand(noUsagePath: true);
409 410
      await flutterCommand.run();
      verify(clock.now()).called(2);
411
      verifyNever(usage.sendTiming(
412 413
                   any, any, any,
                   label: anyNamed('label')));
414
    });
415

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

420
      final FlutterCommandResult commandResult = FlutterCommandResult(
421
        ExitStatus.success,
422
        // nulls should be cleaned up.
423
        timingLabelParts: <String> ['blah1', 'blah2', null, 'blah3'],
424
        endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500),
425 426
      );

427
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
428 429
        commandFunction: () async => commandResult
      );
430 431 432
      await flutterCommand.run();
      verify(clock.now()).called(2);
      expect(
433
        verify(usage.sendTiming(
434
                captureAny, captureAny, captureAny,
435
                label: captureAnyNamed('label'))).captured,
436
        <dynamic>[
437 438
          'flutter',
          'dummy',
439
          const Duration(milliseconds: 500), // FlutterCommandResult's end time used instead.
440
          'success-blah1-blah2-blah3',
441
        ],
442 443 444
      );
    });

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

449
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
450 451 452 453 454
        commandFunction: () async {
          throwToolExit('fail');
          return null; // unreachable
        },
      );
455 456 457 458 459 460 461 462 463

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

        expect(
464
          verify(usage.sendTiming(
465
                  captureAny, captureAny, captureAny,
466 467 468 469 470
                  label: captureAnyNamed('label'))).captured,
          <dynamic>[
            'flutter',
            'dummy',
            const Duration(milliseconds: 1000),
471 472
            'fail',
          ],
473 474
        );
      }
475
    });
476 477 478
  });
}

479
class FakeDeprecatedCommand extends FlutterCommand {
480
  @override
481
  String get description => 'A fake command';
482 483

  @override
484 485 486 487
  String get name => 'deprecated';

  @override
  bool get deprecated => true;
488 489 490

  @override
  Future<FlutterCommandResult> runCommand() async {
491
    return FlutterCommandResult.success();
492
  }
493
}
494

495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
class FakeNullSafeCommand extends FlutterCommand {
  FakeNullSafeCommand() {
    addEnableExperimentation(hide: false);
  }

  @override
  String get description => 'test null safety';

  @override
  String get name => 'safety';

  @override
  Future<FlutterCommandResult> runCommand() async {
    return FlutterCommandResult.success();
  }
}

512
class MockVersion extends Mock implements FlutterVersion {}
513
class MockProcessInfo extends Mock implements ProcessInfo {}
514 515 516 517 518 519
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}

class FakeSignals implements Signals {
  FakeSignals({
    this.subForSigTerm,
    List<ProcessSignal> exitSignals,
520
  }) : delegate = Signals.test(exitSignals: exitSignals);
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539

  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;
}