blob: 9033b580f6dfc68b2f84601ed2c70586aa7139a0 [file] [log] [blame]
// 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:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import 'utils.dart';
void main() {
group('Flutter Command', () {
MockitoCache cache;
MockitoUsage usage;
MockClock clock;
MockProcessInfo mockProcessInfo;
List<int> mockTimes;
setUp(() {
Cache.disableLocking();
cache = MockitoCache();
usage = MockitoUsage();
clock = MockClock();
mockProcessInfo = MockProcessInfo();
when(clock.now()).thenAnswer(
(Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
);
when(mockProcessInfo.maxRss).thenReturn(10);
});
tearDown(() {
Cache.enableLocking();
});
testUsingContext('help text contains global options', () {
final FakeDeprecatedCommand fake = FakeDeprecatedCommand();
createTestCommandRunner(fake);
expect(fake.usage, contains('Global options:\n'));
});
testUsingContext('honors shouldUpdateCache false', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: false);
await flutterCommand.run();
verifyNever(cache.updateAll(any));
expect(flutterCommand.deprecated, isFalse);
expect(flutterCommand.hidden, isFalse);
},
overrides: <Type, Generator>{
Cache: () => cache,
});
testUsingContext('honors shouldUpdateCache true', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true);
await flutterCommand.run();
// First call for universal, second for the rest
expect(
verify(cache.updateAll(captureAny)).captured,
<Set<DevelopmentArtifact>>[
<DevelopmentArtifact>{DevelopmentArtifact.universal},
<DevelopmentArtifact>{},
],
);
},
overrides: <Type, Generator>{
Cache: () => cache,
});
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);
});
testUsingContext('uses the error handling file system', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
expect(globals.fs, isA<ErrorHandlingFileSystem>());
return const FlutterCommandResult(ExitStatus.success);
}
);
await flutterCommand.run();
});
testUsingContext('finds the target file with default values', () async {
globals.fs.file('lib/main.dart').createSync(recursive: true);
final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);
await runner.run(<String>['test']);
expect(fakeTargetCommand.cachedTargetFile, 'lib/main.dart');
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('finds the target file with specified value', () async {
globals.fs.file('lib/foo.dart').createSync(recursive: true);
final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);
await runner.run(<String>['test', '-t', 'lib/foo.dart']);
expect(fakeTargetCommand.cachedTargetFile, 'lib/foo.dart');
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('throws tool exit if specified file does not exist', () async {
final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);
expect(() async => await runner.run(<String>['test', '-t', 'lib/foo.dart']), throwsToolExit());
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
void testUsingCommandContext(String testName, dynamic Function() testBody) {
testUsingContext(testName, testBody, overrides: <Type, Generator>{
ProcessInfo: () => mockProcessInfo,
SystemClock: () => clock,
Usage: () => usage,
});
}
testUsingCommandContext('reports command that results in success', () async {
// 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();
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,
);
});
testUsingCommandContext('reports command that results in warning', () async {
// 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();
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,
);
});
testUsingCommandContext('reports command that results in failure', () async {
// 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 {
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,
);
}
});
testUsingCommandContext('reports command that results in error', () async {
// 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 {
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,
);
}
});
test('FlutterCommandResult.success()', () async {
expect(FlutterCommandResult.success().exitStatus, ExitStatus.success);
});
test('FlutterCommandResult.warning()', () async {
expect(FlutterCommandResult.warning().exitStatus, ExitStatus.warning);
});
testUsingContext('devToolsServerAddress returns parsed uri', () async {
final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions();
await createTestCommandRunner(command).run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'http://127.0.0.1:9105',
]);
expect(command.devToolsServerAddress.toString(), equals('http://127.0.0.1:9105'));
});
testUsingContext('devToolsServerAddress returns null for bad input', () async {
final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'hello-world',
]);
expect(command.devToolsServerAddress, isNull);
await runner.run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'',
]);
expect(command.devToolsServerAddress, isNull);
await runner.run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'9101',
]);
expect(command.devToolsServerAddress, isNull);
await runner.run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'127.0.0.1:9101',
]);
expect(command.devToolsServerAddress, isNull);
});
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;
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,
);
}, overrides: <Type, Generator>{
ProcessInfo: () => mockProcessInfo,
Signals: () => FakeSignals(
subForSigTerm: signalUnderTest,
exitSignals: <ProcessSignal>[signalUnderTest],
),
SystemClock: () => clock,
Usage: () => usage,
});
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 globals.cache.lock();
checkLockCompleter.complete();
final Completer<void> c = Completer<void>();
await c.future;
return null; // unreachable
});
unawaited(flutterCommand.run());
await checkLockCompleter.future;
globals.cache.checkLockAcquired();
signalController.add(mockSignal);
await completer.future;
await globals.cache.lock();
globals.cache.releaseLock();
}, overrides: <Type, Generator>{
ProcessInfo: () => mockProcessInfo,
Signals: () => FakeSignals(
subForSigTerm: signalUnderTest,
exitSignals: <ProcessSignal>[signalUnderTest],
),
Usage: () => usage
});
});
testUsingCommandContext('report execution timing by default', () async {
// Crash if called a third time which is unexpected.
mockTimes = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
await flutterCommand.run();
verify(clock.now()).called(2);
expect(
verify(usage.sendTiming(
captureAny, captureAny, captureAny,
label: captureAnyNamed('label'))).captured,
<dynamic>[
'flutter',
'dummy',
const Duration(milliseconds: 1000),
'fail',
],
);
});
testUsingCommandContext('no timing report without usagePath', () async {
// Crash if called a third time which is unexpected.
mockTimes = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand =
DummyFlutterCommand(noUsagePath: true);
await flutterCommand.run();
verify(clock.now()).called(2);
verifyNever(usage.sendTiming(
any, any, any,
label: anyNamed('label')));
});
testUsingCommandContext('report additional FlutterCommandResult data', () async {
// Crash if called a third time which is unexpected.
mockTimes = <int>[1000, 2000];
final FlutterCommandResult commandResult = FlutterCommandResult(
ExitStatus.success,
// nulls should be cleaned up.
timingLabelParts: <String> ['blah1', 'blah2', null, 'blah3'],
endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500),
);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async => commandResult
);
await flutterCommand.run();
verify(clock.now()).called(2);
expect(
verify(usage.sendTiming(
captureAny, captureAny, captureAny,
label: captureAnyNamed('label'))).captured,
<dynamic>[
'flutter',
'dummy',
const Duration(milliseconds: 500), // FlutterCommandResult's end time used instead.
'success-blah1-blah2-blah3',
],
);
});
testUsingCommandContext('report failed execution timing too', () async {
// 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 {
// Should have still checked time twice.
verify(clock.now()).called(2);
expect(
verify(usage.sendTiming(
captureAny, captureAny, captureAny,
label: captureAnyNamed('label'))).captured,
<dynamic>[
'flutter',
'dummy',
const Duration(milliseconds: 1000),
'fail',
],
);
}
});
testUsingContext('reports null safety analytics when reportNullSafety is true', () async {
globals.fs.file('lib/main.dart')
..createSync(recursive: true)
..writeAsStringSync('// @dart=2.12');
globals.fs.file('pubspec.yaml')
.writeAsStringSync('name: example\n');
globals.fs.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync(r'''
{
"configVersion": 2,
"packages": [
{
"name": "example",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "2.12"
}
],
"generated": "2020-12-02T19:30:53.862346Z",
"generator": "pub",
"generatorVersion": "2.12.0-76.0.dev"
}
''');
final FakeReportingNullSafetyCommand command = FakeReportingNullSafetyCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['test']);
verify(globals.flutterUsage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'runtime-mode', label: 'NullSafetyMode.sound')).called(1);
verify(globals.flutterUsage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'stats', parameters: <String, String>{
'cd49': '1', 'cd50': '1',
})).called(1);
verify(globals.flutterUsage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'language-version', label: '2.12')).called(1);
}, overrides: <Type, Generator>{
Pub: () => FakePub(),
Usage: () => MockitoUsage(),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
});
}
class FakeDeprecatedCommand extends FlutterCommand {
@override
String get description => 'A fake command';
@override
String get name => 'deprecated';
@override
bool get deprecated => true;
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
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();
}
}
class FakeTargetCommand extends FlutterCommand {
FakeTargetCommand() {
usesTargetOption();
}
@override
Future<FlutterCommandResult> runCommand() async {
cachedTargetFile = targetFile;
return FlutterCommandResult.success();
}
String cachedTargetFile;
@override
String get description => '';
@override
String get name => 'test';
}
class FakeReportingNullSafetyCommand extends FlutterCommand {
FakeReportingNullSafetyCommand() {
argParser.addFlag('debug');
argParser.addFlag('release');
argParser.addFlag('jit-release');
argParser.addFlag('profile');
}
@override
String get description => 'test';
@override
String get name => 'test';
@override
bool get shouldRunPub => true;
@override
bool get reportNullSafety => true;
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
class MockVersion extends Mock implements FlutterVersion {}
class MockProcessInfo extends Mock implements ProcessInfo {}
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}
class FakeSignals implements Signals {
FakeSignals({
this.subForSigTerm,
List<ProcessSignal> exitSignals,
}) : delegate = Signals.test(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;
}
class FakePub extends Fake implements Pub {
@override
Future<void> get({
PubContext context,
String directory,
bool skipIfAbsent = false,
bool upgrade = false,
bool offline = false,
bool generateSyntheticPackage = false,
String flutterRootOverride,
bool checkUpToDate = false,
}) async { }
}