Move tools tests into a general.shard directory in preparation to changing how we shard tools tests (#36108)
diff --git a/packages/flutter_tools/test/general.shard/commands/analyze_continuously_test.dart b/packages/flutter_tools/test/general.shard/commands/analyze_continuously_test.dart new file mode 100644 index 0000000..05a49bf --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/analyze_continuously_test.dart
@@ -0,0 +1,105 @@ +// Copyright 2016 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. + +import 'dart:async'; + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/dart/analysis.dart'; +import 'package:flutter_tools/src/dart/pub.dart'; +import 'package:flutter_tools/src/dart/sdk.dart'; +import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + AnalysisServer server; + Directory tempDir; + + setUp(() { + FlutterCommandRunner.initFlutterRoot(); + tempDir = fs.systemTempDirectory.createTempSync('flutter_analysis_test.'); + }); + + tearDown(() { + tryToDelete(tempDir); + return server?.dispose(); + }); + + group('analyze --watch', () { + testUsingContext('AnalysisServer success', () async { + _createSampleProject(tempDir); + + await pubGet(context: PubContext.flutterTests, directory: tempDir.path); + + server = AnalysisServer(dartSdkPath, <String>[tempDir.path]); + + int errorCount = 0; + final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; + server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length); + + await server.start(); + await onDone; + + expect(errorCount, 0); + }, overrides: <Type, Generator>{ + OperatingSystemUtils: () => os, + }); + }); + + testUsingContext('AnalysisServer errors', () async { + _createSampleProject(tempDir, brokenCode: true); + + await pubGet(context: PubContext.flutterTests, directory: tempDir.path); + + server = AnalysisServer(dartSdkPath, <String>[tempDir.path]); + + int errorCount = 0; + final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; + server.onErrors.listen((FileAnalysisErrors errors) { + errorCount += errors.errors.length; + }); + + await server.start(); + await onDone; + + expect(errorCount, greaterThan(0)); + }, overrides: <Type, Generator>{ + OperatingSystemUtils: () => os, + }); + + testUsingContext('Returns no errors when source is error-free', () async { + const String contents = "StringBuffer bar = StringBuffer('baz');"; + tempDir.childFile('main.dart').writeAsStringSync(contents); + server = AnalysisServer(dartSdkPath, <String>[tempDir.path]); + + int errorCount = 0; + final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; + server.onErrors.listen((FileAnalysisErrors errors) { + errorCount += errors.errors.length; + }); + await server.start(); + await onDone; + expect(errorCount, 0); + }, overrides: <Type, Generator>{ + OperatingSystemUtils: () => os, + }); +} + +void _createSampleProject(Directory directory, { bool brokenCode = false }) { + final File pubspecFile = fs.file(fs.path.join(directory.path, 'pubspec.yaml')); + pubspecFile.writeAsStringSync(''' +name: foo_project +'''); + + final File dartFile = fs.file(fs.path.join(directory.path, 'lib', 'main.dart')); + dartFile.parent.createSync(); + dartFile.writeAsStringSync(''' +void main() { + print('hello world'); + ${brokenCode ? 'prints("hello world");' : ''} +} +'''); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/analyze_once_test.dart b/packages/flutter_tools/test/general.shard/commands/analyze_once_test.dart new file mode 100644 index 0000000..d783d64 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/analyze_once_test.dart
@@ -0,0 +1,242 @@ +// 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. + +import 'dart:async'; + +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/analyze.dart'; +import 'package:flutter_tools/src/commands/create.dart'; +import 'package:flutter_tools/src/runner/flutter_command.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +/// Test case timeout for tests involving project analysis. +const Timeout allowForSlowAnalyzeTests = Timeout.factor(5.0); + +final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false; +final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{ + Platform: _kNoColorTerminalPlatform, +}; + +void main() { + final String analyzerSeparator = platform.isWindows ? '-' : '•'; + + group('analyze once', () { + Directory tempDir; + String projectPath; + File libMain; + + setUpAll(() { + Cache.disableLocking(); + tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_1.').absolute; + projectPath = fs.path.join(tempDir.path, 'flutter_project'); + libMain = fs.file(fs.path.join(projectPath, 'lib', 'main.dart')); + }); + + tearDownAll(() { + tryToDelete(tempDir); + }); + + // Create a project to be analyzed + testUsingContext('flutter create', () async { + await runCommand( + command: CreateCommand(), + arguments: <String>['--no-wrap', 'create', projectPath], + statusTextContains: <String>[ + 'All done!', + 'Your application code is in ${fs.path.normalize(fs.path.join(fs.path.relative(projectPath), 'lib', 'main.dart'))}', + ], + ); + expect(libMain.existsSync(), isTrue); + }, timeout: allowForRemotePubInvocation); + + // Analyze in the current directory - no arguments + testUsingContext('working directory', () async { + await runCommand( + command: AnalyzeCommand(workingDirectory: fs.directory(projectPath)), + arguments: <String>['analyze'], + statusTextContains: <String>['No issues found!'], + ); + }, timeout: allowForSlowAnalyzeTests); + + // Analyze a specific file outside the current directory + testUsingContext('passing one file throws', () async { + await runCommand( + command: AnalyzeCommand(), + arguments: <String>['analyze', libMain.path], + toolExit: true, + exitMessageContains: 'is not a directory', + ); + }); + + // Analyze in the current directory - no arguments + testUsingContext('working directory with errors', () async { + // Break the code to produce the "The parameter 'onPressed' is required" hint + // that is upgraded to a warning in package:flutter/analysis_options_user.yaml + // to assert that we are using the default Flutter analysis options. + // Also insert a statement that should not trigger a lint here + // but will trigger a lint later on when an analysis_options.yaml is added. + String source = await libMain.readAsString(); + source = source.replaceFirst( + 'onPressed: _incrementCounter,', + '// onPressed: _incrementCounter,', + ); + source = source.replaceFirst( + '_counter++;', + '_counter++; throw "an error message";', + ); + await libMain.writeAsString(source); + + // Analyze in the current directory - no arguments + await runCommand( + command: AnalyzeCommand(workingDirectory: fs.directory(projectPath)), + arguments: <String>['analyze'], + statusTextContains: <String>[ + 'Analyzing', + 'warning $analyzerSeparator The parameter \'onPressed\' is required', + 'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used', + ], + exitMessageContains: '2 issues found.', + toolExit: true, + ); + }, timeout: allowForSlowAnalyzeTests, overrides: noColorTerminalOverride); + + // Analyze in the current directory - no arguments + testUsingContext('working directory with local options', () async { + // Insert an analysis_options.yaml file in the project + // which will trigger a lint for broken code that was inserted earlier + final File optionsFile = fs.file(fs.path.join(projectPath, 'analysis_options.yaml')); + await optionsFile.writeAsString(''' + include: package:flutter/analysis_options_user.yaml + linter: + rules: + - only_throw_errors + '''); + + // Analyze in the current directory - no arguments + await runCommand( + command: AnalyzeCommand(workingDirectory: fs.directory(projectPath)), + arguments: <String>['analyze'], + statusTextContains: <String>[ + 'Analyzing', + 'warning $analyzerSeparator The parameter \'onPressed\' is required', + 'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used', + 'info $analyzerSeparator Only throw instances of classes extending either Exception or Error', + ], + exitMessageContains: '3 issues found.', + toolExit: true, + ); + }, timeout: allowForSlowAnalyzeTests, overrides: noColorTerminalOverride); + + testUsingContext('no duplicate issues', () async { + final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_2.').absolute; + + try { + final File foo = fs.file(fs.path.join(tempDir.path, 'foo.dart')); + foo.writeAsStringSync(''' +import 'bar.dart'; + +void foo() => bar(); +'''); + + final File bar = fs.file(fs.path.join(tempDir.path, 'bar.dart')); + bar.writeAsStringSync(''' +import 'dart:async'; // unused + +void bar() { +} +'''); + + // Analyze in the current directory - no arguments + await runCommand( + command: AnalyzeCommand(workingDirectory: tempDir), + arguments: <String>['analyze'], + statusTextContains: <String>[ + 'Analyzing', + ], + exitMessageContains: '1 issue found.', + toolExit: true, + ); + } finally { + tryToDelete(tempDir); + } + }, overrides: noColorTerminalOverride); + + testUsingContext('returns no issues when source is error-free', () async { + const String contents = ''' +StringBuffer bar = StringBuffer('baz'); +'''; + final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_3.'); + tempDir.childFile('main.dart').writeAsStringSync(contents); + try { + await runCommand( + command: AnalyzeCommand(workingDirectory: fs.directory(tempDir)), + arguments: <String>['analyze'], + statusTextContains: <String>['No issues found!'], + ); + } finally { + tryToDelete(tempDir); + } + }, overrides: noColorTerminalOverride); + + testUsingContext('returns no issues for todo comments', () async { + const String contents = ''' +// TODO(foobar): +StringBuffer bar = StringBuffer('baz'); +'''; + final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_4.'); + tempDir.childFile('main.dart').writeAsStringSync(contents); + try { + await runCommand( + command: AnalyzeCommand(workingDirectory: fs.directory(tempDir)), + arguments: <String>['analyze'], + statusTextContains: <String>['No issues found!'], + ); + } finally { + tryToDelete(tempDir); + } + }, overrides: noColorTerminalOverride); + }); +} + +void assertContains(String text, List<String> patterns) { + if (patterns == null) { + expect(text, isEmpty); + } else { + for (String pattern in patterns) { + expect(text, contains(pattern)); + } + } +} + +Future<void> runCommand({ + FlutterCommand command, + List<String> arguments, + List<String> statusTextContains, + List<String> errorTextContains, + bool toolExit = false, + String exitMessageContains, +}) async { + try { + arguments.insert(0, '--flutter-root=${Cache.flutterRoot}'); + await createTestCommandRunner(command).run(arguments); + expect(toolExit, isFalse, reason: 'Expected ToolExit exception'); + } on ToolExit catch (e) { + if (!toolExit) { + testLogger.clear(); + rethrow; + } + if (exitMessageContains != null) { + expect(e.message, contains(exitMessageContains)); + } + } + assertContains(testLogger.statusText, statusTextContains); + assertContains(testLogger.errorText, errorTextContains); + + testLogger.clear(); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/analyze_test.dart b/packages/flutter_tools/test/general.shard/commands/analyze_test.dart new file mode 100644 index 0000000..ee24615 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/analyze_test.dart
@@ -0,0 +1,53 @@ +// Copyright 2016 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. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/analyze_base.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +const String _kFlutterRoot = '/data/flutter'; + +void main() { + FileSystem fs; + Directory tempDir; + + setUp(() { + fs = MemoryFileSystem(); + fs.directory(_kFlutterRoot).createSync(recursive: true); + Cache.flutterRoot = _kFlutterRoot; + tempDir = fs.systemTempDirectory.createTempSync('flutter_analysis_test.'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + group('analyze', () { + testUsingContext('inRepo', () { + // Absolute paths + expect(inRepo(<String>[tempDir.path]), isFalse); + expect(inRepo(<String>[fs.path.join(tempDir.path, 'foo')]), isFalse); + expect(inRepo(<String>[Cache.flutterRoot]), isTrue); + expect(inRepo(<String>[fs.path.join(Cache.flutterRoot, 'foo')]), isTrue); + + // Relative paths + fs.currentDirectory = Cache.flutterRoot; + expect(inRepo(<String>['.']), isTrue); + expect(inRepo(<String>['foo']), isTrue); + fs.currentDirectory = tempDir.path; + expect(inRepo(<String>['.']), isFalse); + expect(inRepo(<String>['foo']), isFalse); + + // Ensure no exceptions + inRepo(null); + inRepo(<String>[]); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + }); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart new file mode 100644 index 0000000..e18ebb5 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
@@ -0,0 +1,84 @@ +// Copyright 2019 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. + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/assemble.dart'; +import 'package:flutter_tools/src/globals.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + +void main() { + group('Assemble', () { + Testbed testbed; + MockBuildSystem mockBuildSystem; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockBuildSystem = MockBuildSystem(); + testbed = Testbed(overrides: <Type, Generator>{ + BuildSystem: () => mockBuildSystem, + }); + }); + + test('Can list the output directory relative to project root', () => testbed.run(() async { + final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(<String>['assemble', '--flutter-root=.', 'build-dir', '-dBuildMode=debug']); + final BufferLogger bufferLogger = logger; + final Environment environment = Environment( + defines: <String, String>{ + 'BuildMode': 'debug' + }, projectDir: fs.currentDirectory, + buildDir: fs.directory(getBuildDirectory()), + ); + + expect(bufferLogger.statusText.trim(), + fs.path.relative(environment.buildDir.path, from: fs.currentDirectory.path)); + })); + + test('Can describe a target', () => testbed.run(() async { + when(mockBuildSystem.describe('foobar', any)).thenReturn(<Map<String, Object>>[ + <String, Object>{'fizz': 'bar'}, + ]); + final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(<String>['assemble', '--flutter-root=.', 'describe', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), '[{"fizz":"bar"}]'); + })); + + test('Can describe a target\'s inputs', () => testbed.run(() async { + when(mockBuildSystem.describe('foobar', any)).thenReturn(<Map<String, Object>>[ + <String, Object>{'name': 'foobar', 'inputs': <String>['bar', 'baz']}, + ]); + final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(<String>['assemble', '--flutter-root=.', 'inputs', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), 'bar\nbaz'); + })); + + test('Can run a build', () => testbed.run(() async { + when(mockBuildSystem.build('foobar', any, any)).thenAnswer((Invocation invocation) async { + return BuildResult(true, const <String, ExceptionMeasurement>{}, const <String, PerformanceMeasurement>{}); + }); + final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand()); + await commandRunner.run(<String>['assemble', 'run', 'foobar']); + final BufferLogger bufferLogger = logger; + + expect(bufferLogger.statusText.trim(), 'build succeeded'); + })); + }); +} + +class MockBuildSystem extends Mock implements BuildSystem {}
diff --git a/packages/flutter_tools/test/general.shard/commands/attach_test.dart b/packages/flutter_tools/test/general.shard/commands/attach_test.dart new file mode 100644 index 0000000..c708f1e --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/attach_test.dart
@@ -0,0 +1,644 @@ +// Copyright 2018 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. + +import 'dart:async'; + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/attach.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/run_hot.dart'; +import 'package:meta/meta.dart'; +import 'package:mockito/mockito.dart'; +import 'package:multicast_dns/multicast_dns.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('attach', () { + StreamLogger logger; + FileSystem testFileSystem; + + setUp(() { + Cache.disableLocking(); + logger = StreamLogger(); + testFileSystem = MemoryFileSystem( + style: platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, + ); + testFileSystem.directory('lib').createSync(); + testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync(); + }); + + group('with one device and no specified target file', () { + const int devicePort = 499; + const int hostPort = 42; + + MockDeviceLogReader mockLogReader; + MockPortForwarder portForwarder; + MockAndroidDevice device; + + setUp(() { + mockLogReader = MockDeviceLogReader(); + portForwarder = MockPortForwarder(); + device = MockAndroidDevice(); + when(device.getLogReader()).thenAnswer((_) { + // Now that the reader is used, start writing messages to it. + Timer.run(() { + mockLogReader.addLine('Foo'); + mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort'); + }); + + return mockLogReader; + }); + when(device.portForwarder) + .thenReturn(portForwarder); + when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort'))) + .thenAnswer((_) async => hostPort); + when(portForwarder.forwardedPorts) + .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]); + when(portForwarder.unforward(any)) + .thenAnswer((_) async => null); + + // We cannot add the device to a device manager because that is + // only enabled by the context of each testUsingContext call. + // + // Instead each test will add the device to the device manager + // on its own. + }); + + tearDown(() { + mockLogReader.dispose(); + }); + + testUsingContext('finds observatory port and forwards', () async { + testDeviceManager.addDevice(device); + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[stdout] Done.') { + // The "Done." message is output by the AttachCommand when it's done. + completer.complete(); + } + }); + final Future<void> task = createTestCommandRunner(AttachCommand()).run(<String>['attach']); + await completer.future; + verify( + portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')), + ).called(1); + await expectLoggerInterruptEndsTask(task, logger); + await loggerSubscription.cancel(); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + Logger: () => logger, + }); + + testUsingContext('accepts filesystem parameters', () async { + testDeviceManager.addDevice(device); + + const String filesystemScheme = 'foo'; + const String filesystemRoot = '/build-output/'; + const String projectRoot = '/build-output/project-root'; + const String outputDill = '/tmp/output.dill'; + + final MockHotRunner mockHotRunner = MockHotRunner(); + when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'))) + .thenAnswer((_) async => 0); + + final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory(); + when( + mockHotRunnerFactory.build( + any, + target: anyNamed('target'), + projectRootPath: anyNamed('projectRootPath'), + dillOutputPath: anyNamed('dillOutputPath'), + debuggingOptions: anyNamed('debuggingOptions'), + packagesFilePath: anyNamed('packagesFilePath'), + usesTerminalUi: anyNamed('usesTerminalUi'), + flutterProject: anyNamed('flutterProject'), + ipv6: false, + ), + ).thenReturn(mockHotRunner); + + final AttachCommand command = AttachCommand( + hotRunnerFactory: mockHotRunnerFactory, + ); + await createTestCommandRunner(command).run(<String>[ + 'attach', + '--filesystem-scheme', + filesystemScheme, + '--filesystem-root', + filesystemRoot, + '--project-root', + projectRoot, + '--output-dill', + outputDill, + '-v', // enables verbose logging + ]); + + // Validate the attach call built a mock runner with the right + // project root and output dill. + final VerificationResult verificationResult = verify( + mockHotRunnerFactory.build( + captureAny, + target: anyNamed('target'), + projectRootPath: projectRoot, + dillOutputPath: outputDill, + debuggingOptions: anyNamed('debuggingOptions'), + packagesFilePath: anyNamed('packagesFilePath'), + usesTerminalUi: anyNamed('usesTerminalUi'), + flutterProject: anyNamed('flutterProject'), + ipv6: false, + ), + )..called(1); + + final List<FlutterDevice> flutterDevices = verificationResult.captured.first; + expect(flutterDevices, hasLength(1)); + + // Validate that the attach call built a flutter device with the right + // output dill, filesystem scheme, and filesystem root. + final FlutterDevice flutterDevice = flutterDevices.first; + + expect(flutterDevice.dillOutputPath, outputDill); + expect(flutterDevice.fileSystemScheme, filesystemScheme); + expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + }); + + testUsingContext('exits when ipv6 is specified and debug-port is not', () async { + testDeviceManager.addDevice(device); + + final AttachCommand command = AttachCommand(); + await expectLater( + createTestCommandRunner(command).run(<String>['attach', '--ipv6']), + throwsToolExit( + message: 'When the --debug-port or --debug-uri is unknown, this command determines ' + 'the value of --ipv6 on its own.', + ), + ); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + },); + + testUsingContext('exits when observatory-port is specified and debug-port is not', () async { + testDeviceManager.addDevice(device); + + final AttachCommand command = AttachCommand(); + await expectLater( + createTestCommandRunner(command).run(<String>['attach', '--observatory-port', '100']), + throwsToolExit( + message: 'When the --debug-port or --debug-uri is unknown, this command does not use ' + 'the value of --observatory-port.', + ), + ); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + },); + }); + + + testUsingContext('selects specified target', () async { + const int devicePort = 499; + const int hostPort = 42; + final MockDeviceLogReader mockLogReader = MockDeviceLogReader(); + final MockPortForwarder portForwarder = MockPortForwarder(); + final MockAndroidDevice device = MockAndroidDevice(); + final MockHotRunner mockHotRunner = MockHotRunner(); + final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory(); + when(device.portForwarder) + .thenReturn(portForwarder); + when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort'))) + .thenAnswer((_) async => hostPort); + when(portForwarder.forwardedPorts) + .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]); + when(portForwarder.unforward(any)) + .thenAnswer((_) async => null); + when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'))) + .thenAnswer((_) async => 0); + when(mockHotRunnerFactory.build( + any, + target: anyNamed('target'), + debuggingOptions: anyNamed('debuggingOptions'), + packagesFilePath: anyNamed('packagesFilePath'), + usesTerminalUi: anyNamed('usesTerminalUi'), + flutterProject: anyNamed('flutterProject'), + ipv6: false, + )).thenReturn(mockHotRunner); + + testDeviceManager.addDevice(device); + when(device.getLogReader()) + .thenAnswer((_) { + // Now that the reader is used, start writing messages to it. + Timer.run(() { + mockLogReader.addLine('Foo'); + mockLogReader.addLine( + 'Observatory listening on http://127.0.0.1:$devicePort'); + }); + return mockLogReader; + }); + final File foo = fs.file('lib/foo.dart') + ..createSync(); + + // Delete the main.dart file to be sure that attach works without it. + fs.file(fs.path.join('lib', 'main.dart')).deleteSync(); + + final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory); + await createTestCommandRunner(command).run(<String>['attach', '-t', foo.path, '-v']); + + verify(mockHotRunnerFactory.build( + any, + target: foo.path, + debuggingOptions: anyNamed('debuggingOptions'), + packagesFilePath: anyNamed('packagesFilePath'), + usesTerminalUi: anyNamed('usesTerminalUi'), + flutterProject: anyNamed('flutterProject'), + ipv6: false, + )).called(1); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + }); + + group('forwarding to given port', () { + const int devicePort = 499; + const int hostPort = 42; + MockPortForwarder portForwarder; + MockAndroidDevice device; + + setUp(() { + portForwarder = MockPortForwarder(); + device = MockAndroidDevice(); + + when(device.portForwarder) + .thenReturn(portForwarder); + when(portForwarder.forward(devicePort)) + .thenAnswer((_) async => hostPort); + when(portForwarder.forwardedPorts) + .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]); + when(portForwarder.unforward(any)) + .thenAnswer((_) async => null); + }); + + testUsingContext('succeeds in ipv4 mode', () async { + testDeviceManager.addDevice(device); + + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') { + // Wait until resident_runner.dart tries to connect. + // There's nothing to connect _to_, so that's as far as we care to go. + completer.complete(); + } + }); + final Future<void> task = createTestCommandRunner(AttachCommand()) + .run(<String>['attach', '--debug-port', '$devicePort']); + await completer.future; + verify(portForwarder.forward(devicePort)).called(1); + + await expectLoggerInterruptEndsTask(task, logger); + await loggerSubscription.cancel(); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + Logger: () => logger, + }); + + testUsingContext('succeeds in ipv6 mode', () async { + testDeviceManager.addDevice(device); + + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') { + // Wait until resident_runner.dart tries to connect. + // There's nothing to connect _to_, so that's as far as we care to go. + completer.complete(); + } + }); + final Future<void> task = createTestCommandRunner(AttachCommand()) + .run(<String>['attach', '--debug-port', '$devicePort', '--ipv6']); + await completer.future; + verify(portForwarder.forward(devicePort)).called(1); + + await expectLoggerInterruptEndsTask(task, logger); + await loggerSubscription.cancel(); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + Logger: () => logger, + }); + + testUsingContext('skips in ipv4 mode with a provided observatory port', () async { + testDeviceManager.addDevice(device); + + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') { + // Wait until resident_runner.dart tries to connect. + // There's nothing to connect _to_, so that's as far as we care to go. + completer.complete(); + } + }); + final Future<void> task = createTestCommandRunner(AttachCommand()).run( + <String>[ + 'attach', + '--debug-port', + '$devicePort', + '--observatory-port', + '$hostPort', + ], + ); + await completer.future; + verifyNever(portForwarder.forward(devicePort)); + + await expectLoggerInterruptEndsTask(task, logger); + await loggerSubscription.cancel(); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + Logger: () => logger, + }); + + testUsingContext('skips in ipv6 mode with a provided observatory port', () async { + testDeviceManager.addDevice(device); + + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') { + // Wait until resident_runner.dart tries to connect. + // There's nothing to connect _to_, so that's as far as we care to go. + completer.complete(); + } + }); + final Future<void> task = createTestCommandRunner(AttachCommand()).run( + <String>[ + 'attach', + '--debug-port', + '$devicePort', + '--observatory-port', + '$hostPort', + '--ipv6', + ], + ); + await completer.future; + verifyNever(portForwarder.forward(devicePort)); + + await expectLoggerInterruptEndsTask(task, logger); + await loggerSubscription.cancel(); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + Logger: () => logger, + }); + }); + + testUsingContext('exits when no device connected', () async { + final AttachCommand command = AttachCommand(); + await expectLater( + createTestCommandRunner(command).run(<String>['attach']), + throwsA(isInstanceOf<ToolExit>()), + ); + expect(testLogger.statusText, contains('No supported devices connected')); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + }); + + testUsingContext('exits when multiple devices connected', () async { + Device aDeviceWithId(String id) { + final MockAndroidDevice device = MockAndroidDevice(); + when(device.name).thenReturn('d$id'); + when(device.id).thenReturn(id); + when(device.isLocalEmulator).thenAnswer((_) async => false); + when(device.sdkNameAndVersion).thenAnswer((_) async => 'Android 46'); + return device; + } + + final AttachCommand command = AttachCommand(); + testDeviceManager.addDevice(aDeviceWithId('xx1')); + testDeviceManager.addDevice(aDeviceWithId('yy2')); + await expectLater( + createTestCommandRunner(command).run(<String>['attach']), + throwsA(isInstanceOf<ToolExit>()), + ); + expect(testLogger.statusText, contains('More than one device')); + expect(testLogger.statusText, contains('xx1')); + expect(testLogger.statusText, contains('yy2')); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + }); + }); + + group('mDNS Discovery', () { + final int year3000 = DateTime(3000).millisecondsSinceEpoch; + + MDnsClient getMockClient( + List<PtrResourceRecord> ptrRecords, + Map<String, List<SrvResourceRecord>> srvResponse, + ) { + final MDnsClient client = MockMDnsClient(); + + when(client.lookup<PtrResourceRecord>( + ResourceRecordQuery.serverPointer(MDnsObservatoryDiscovery.dartObservatoryName), + )).thenAnswer((_) => Stream<PtrResourceRecord>.fromIterable(ptrRecords)); + + for (final MapEntry<String, List<SrvResourceRecord>> entry in srvResponse.entries) { + when(client.lookup<SrvResourceRecord>( + ResourceRecordQuery.service(entry.key), + )).thenAnswer((_) => Stream<SrvResourceRecord>.fromIterable(entry.value)); + } + return client; + } + + testUsingContext('No ports available', () async { + final MDnsClient client = getMockClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query())?.port; + expect(port, isNull); + }); + + testUsingContext('One port available, no appId', () async { + final MDnsClient client = getMockClient( + <PtrResourceRecord>[ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + ], + <String, List<SrvResourceRecord>>{ + 'bar': <SrvResourceRecord>[ + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query())?.port; + expect(port, 123); + }); + + testUsingContext('Multiple ports available, without appId', () async { + final MDnsClient client = getMockClient( + <PtrResourceRecord>[ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + PtrResourceRecord('baz', year3000, domainName: 'fiz'), + ], + <String, List<SrvResourceRecord>>{ + 'bar': <SrvResourceRecord>[ + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'fiz': <SrvResourceRecord>[ + SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + expect(() => portDiscovery.query(), throwsToolExit()); + }); + + testUsingContext('Multiple ports available, with appId', () async { + final MDnsClient client = getMockClient( + <PtrResourceRecord>[ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + PtrResourceRecord('baz', year3000, domainName: 'fiz'), + ], + <String, List<SrvResourceRecord>>{ + 'bar': <SrvResourceRecord>[ + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'fiz': <SrvResourceRecord>[ + SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port; + expect(port, 321); + }); + + testUsingContext('Multiple ports available per process, with appId', () async { + final MDnsClient client = getMockClient( + <PtrResourceRecord>[ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + PtrResourceRecord('baz', year3000, domainName: 'fiz'), + ], + <String, List<SrvResourceRecord>>{ + 'bar': <SrvResourceRecord>[ + SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'), + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'fiz': <SrvResourceRecord>[ + SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'), + SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; + expect(port, 1234); + }); + + testUsingContext('Query returns null', () async { + final MDnsClient client = getMockClient( + <PtrResourceRecord>[], + <String, List<SrvResourceRecord>>{}, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; + expect(port, isNull); + }); + }); +} + +class MockMDnsClient extends Mock implements MDnsClient {} + +class MockPortForwarder extends Mock implements DevicePortForwarder {} + +class MockHotRunner extends Mock implements HotRunner {} + +class MockHotRunnerFactory extends Mock implements HotRunnerFactory {} + +class StreamLogger extends Logger { + @override + bool get isVerbose => true; + + @override + void printError( + String message, { + StackTrace stackTrace, + bool emphasis, + TerminalColor color, + int indent, + int hangingIndent, + bool wrap, + }) { + _log('[stderr] $message'); + } + + @override + void printStatus( + String message, { + bool emphasis, + TerminalColor color, + bool newline, + int indent, + int hangingIndent, + bool wrap, + }) { + _log('[stdout] $message'); + } + + @override + void printTrace(String message) { + _log('[verbose] $message'); + } + + @override + Status startProgress( + String message, { + @required Duration timeout, + String progressId, + bool multilineOutput = false, + int progressIndicatorPadding = kDefaultStatusPadding, + }) { + _log('[progress] $message'); + return SilentStatus(timeout: timeout)..start(); + } + + bool _interrupt = false; + + void interrupt() { + _interrupt = true; + } + + final StreamController<String> _controller = StreamController<String>.broadcast(); + + void _log(String message) { + _controller.add(message); + if (_interrupt) { + _interrupt = false; + throw const LoggerInterrupted(); + } + } + + Stream<String> get stream => _controller.stream; +} + +class LoggerInterrupted implements Exception { + const LoggerInterrupted(); +} + +Future<void> expectLoggerInterruptEndsTask(Future<void> task, StreamLogger logger) async { + logger.interrupt(); // an exception during the task should cause it to fail... + try { + await task; + expect(false, isTrue); // (shouldn't reach here) + } on ToolExit catch (error) { + expect(error.exitCode, 2); // ...with exit code 2. + } +}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart new file mode 100644 index 0000000..6c6c8ad --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart
@@ -0,0 +1,96 @@ +// Copyright 2019 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. + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/build_bundle.dart'; +import 'package:flutter_tools/src/bundle.dart'; +import 'package:flutter_tools/src/usage.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + Cache.disableLocking(); + + group('getUsage', () { + Directory tempDir; + MockBundleBuilder mockBundleBuilder; + + setUp(() { + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + + mockBundleBuilder = MockBundleBuilder(); + when( + mockBundleBuilder.build( + platform: anyNamed('platform'), + buildMode: anyNamed('buildMode'), + mainPath: anyNamed('mainPath'), + manifestPath: anyNamed('manifestPath'), + applicationKernelFilePath: anyNamed('applicationKernelFilePath'), + depfilePath: anyNamed('depfilePath'), + privateKeyPath: anyNamed('privateKeyPath'), + assetDirPath: anyNamed('assetDirPath'), + packagesPath: anyNamed('packagesPath'), + precompiledSnapshot: anyNamed('precompiledSnapshot'), + reportLicensedPackages: anyNamed('reportLicensedPackages'), + trackWidgetCreation: anyNamed('trackWidgetCreation'), + extraFrontEndOptions: anyNamed('extraFrontEndOptions'), + extraGenSnapshotOptions: anyNamed('extraGenSnapshotOptions'), + fileSystemRoots: anyNamed('fileSystemRoots'), + fileSystemScheme: anyNamed('fileSystemScheme'), + ), + ).thenAnswer((_) => Future<void>.value()); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + Future<BuildBundleCommand> runCommandIn(String projectPath, { List<String> arguments }) async { + final BuildBundleCommand command = BuildBundleCommand(bundleBuilder: mockBundleBuilder); + final CommandRunner<void> runner = createTestCommandRunner(command); + await runner.run(<String>[ + 'bundle', + ...?arguments, + '--target=$projectPath/lib/main.dart', + ]); + return command; + } + + testUsingContext('indicate that project is a module', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + + final BuildBundleCommand command = await runCommandIn(projectPath); + + expect(await command.usageValues, + containsPair(kCommandBuildBundleIsModule, 'true')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('indicate that project is not a module', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=app']); + + final BuildBundleCommand command = await runCommandIn(projectPath); + + expect(await command.usageValues, + containsPair(kCommandBuildBundleIsModule, 'false')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('indicate the target platform', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=app']); + + final BuildBundleCommand command = await runCommandIn(projectPath); + + expect(await command.usageValues, + containsPair(kCommandBuildBundleTargetPlatform, 'android-arm')); + }, timeout: allowForCreateFlutterProject); + }); +} + +class MockBundleBuilder extends Mock implements BundleBuilder {}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_fuchsia_test.dart b/packages/flutter_tools/test/general.shard/commands/build_fuchsia_test.dart new file mode 100644 index 0000000..3734d04 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_fuchsia_test.dart
@@ -0,0 +1,237 @@ +// Copyright 2019 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. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/build.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:meta/meta.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + Cache.disableLocking(); + + MemoryFileSystem memoryFileSystem; + MockPlatform linuxPlatform; + MockPlatform windowsPlatform; + MockFuchsiaSdk fuchsiaSdk; + MockFuchsiaArtifacts fuchsiaArtifacts; + MockFuchsiaArtifacts fuchsiaArtifactsNoCompiler; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + linuxPlatform = MockPlatform(); + windowsPlatform = MockPlatform(); + fuchsiaSdk = MockFuchsiaSdk(); + fuchsiaArtifacts = MockFuchsiaArtifacts(); + fuchsiaArtifactsNoCompiler = MockFuchsiaArtifacts(); + + when(linuxPlatform.isLinux).thenReturn(true); + when(windowsPlatform.isWindows).thenReturn(true); + when(windowsPlatform.isLinux).thenReturn(false); + when(windowsPlatform.isMacOS).thenReturn(false); + when(fuchsiaArtifacts.kernelCompiler).thenReturn(MockFile()); + when(fuchsiaArtifactsNoCompiler.kernelCompiler).thenReturn(null); + }); + + group('Fuchsia build fails gracefully when', () { + testUsingContext('there is no Fuchsia project', + () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + expect( + createTestCommandRunner(command) + .run(const <String>['build', 'fuchsia']), + throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifacts, + }); + + testUsingContext('there is no cmx file', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.directory('fuchsia').createSync(recursive: true); + fs.file('.packages').createSync(); + fs.file('pubspec.yaml').createSync(); + + expect( + createTestCommandRunner(command) + .run(const <String>['build', 'fuchsia']), + throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifacts, + }); + + testUsingContext('on Windows platform', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + const String appName = 'app_name'; + fs + .file(fs.path.join('fuchsia', 'meta', '$appName.cmx')) + ..createSync(recursive: true) + ..writeAsStringSync('{}'); + fs.file('.packages').createSync(); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); + + expect( + createTestCommandRunner(command) + .run(const <String>['build', 'fuchsia']), + throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => windowsPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifacts, + }); + + testUsingContext('there is no Fuchsia kernel compiler', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + const String appName = 'app_name'; + fs + .file(fs.path.join('fuchsia', 'meta', '$appName.cmx')) + ..createSync(recursive: true) + ..writeAsStringSync('{}'); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); + expect( + createTestCommandRunner(command) + .run(const <String>['build', 'fuchsia']), + throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifactsNoCompiler, + }); + }); + + testUsingContext('Fuchsia build parts fit together right', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + const String appName = 'app_name'; + fs + .file(fs.path.join('fuchsia', 'meta', '$appName.cmx')) + ..createSync(recursive: true) + ..writeAsStringSync('{}'); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); + + await createTestCommandRunner(command) + .run(const <String>['build', 'fuchsia']); + final String farPath = + fs.path.join(getFuchsiaBuildDirectory(), 'pkg', 'app_name-0.far'); + expect(fs.file(farPath).existsSync(), isTrue); + }, overrides: <Type, Generator>{ + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaSdk: () => fuchsiaSdk, + }); +} + +class MockPlatform extends Mock implements Platform { + @override + Map<String, String> environment = <String, String>{ + 'FLUTTER_ROOT': '/', + }; +} + +class MockFuchsiaPM extends Mock implements FuchsiaPM { + String _appName; + + @override + Future<bool> init(String buildPath, String appName) async { + if (!fs.directory(buildPath).existsSync()) { + return false; + } + fs + .file(fs.path.join(buildPath, 'meta', 'package')) + .createSync(recursive: true); + _appName = appName; + return true; + } + + @override + Future<bool> genkey(String buildPath, String outKeyPath) async { + if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync()) { + return false; + } + fs.file(outKeyPath).createSync(recursive: true); + return true; + } + + @override + Future<bool> build( + String buildPath, String keyPath, String manifestPath) async { + if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() || + !fs.file(keyPath).existsSync() || + !fs.file(manifestPath).existsSync()) { + return false; + } + fs.file(fs.path.join(buildPath, 'meta.far')).createSync(recursive: true); + return true; + } + + @override + Future<bool> archive( + String buildPath, String keyPath, String manifestPath) async { + if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() || + !fs.file(keyPath).existsSync() || + !fs.file(manifestPath).existsSync()) { + return false; + } + if (_appName == null) { + return false; + } + fs + .file(fs.path.join(buildPath, '$_appName-0.far')) + .createSync(recursive: true); + return true; + } +} + +class MockFuchsiaKernelCompiler extends Mock implements FuchsiaKernelCompiler { + @override + Future<void> build({ + @required FuchsiaProject fuchsiaProject, + @required String target, // E.g., lib/main.dart + BuildInfo buildInfo = BuildInfo.debug, + }) async { + final String outDir = getFuchsiaBuildDirectory(); + final String appName = fuchsiaProject.project.manifest.appName; + final String manifestPath = fs.path.join(outDir, '$appName.dilpmanifest'); + fs.file(manifestPath).createSync(recursive: true); + } +} + +class MockFuchsiaSdk extends Mock implements FuchsiaSdk { + @override + final FuchsiaPM fuchsiaPM = MockFuchsiaPM(); + + @override + final FuchsiaKernelCompiler fuchsiaKernelCompiler = + MockFuchsiaKernelCompiler(); +} + +class MockFile extends Mock implements File {} + +class MockFuchsiaArtifacts extends Mock implements FuchsiaArtifacts {}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_linux_test.dart b/packages/flutter_tools/test/general.shard/commands/build_linux_test.dart new file mode 100644 index 0000000..638a20b --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_linux_test.dart
@@ -0,0 +1,125 @@ +// Copyright 2019 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. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/build.dart'; +import 'package:flutter_tools/src/linux/makefile.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + MockProcessManager mockProcessManager; + MockProcess mockProcess; + MockPlatform linuxPlatform; + MockPlatform notLinuxPlatform; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockProcessManager = MockProcessManager(); + mockProcess = MockProcess(); + linuxPlatform = MockPlatform(); + notLinuxPlatform = MockPlatform(); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { + return 0; + }); + when(mockProcess.stderr).thenAnswer((Invocation invocation) { + return const Stream<List<int>>.empty(); + }); + when(mockProcess.stdout).thenAnswer((Invocation invocation) { + return const Stream<List<int>>.empty(); + }); + when(linuxPlatform.isLinux).thenReturn(true); + when(notLinuxPlatform.isLinux).thenReturn(false); + }); + + testUsingContext('Linux build fails when there is no linux project', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + expect(createTestCommandRunner(command).run( + const <String>['build', 'linux'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => linuxPlatform, + FileSystem: () => MemoryFileSystem(), + }); + + testUsingContext('Linux build fails on non-linux platform', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.file('linux/build.sh').createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + + expect(createTestCommandRunner(command).run( + const <String>['build', 'linux'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => notLinuxPlatform, + FileSystem: () => MemoryFileSystem(), + }); + + testUsingContext('Linux build invokes make and writes temporary files', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.file('linux/build.sh').createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + + when(mockProcessManager.start(<String>[ + 'make', + '-C', + '/linux', + ], runInShell: true)).thenAnswer((Invocation invocation) async { + return mockProcess; + }); + + await createTestCommandRunner(command).run( + const <String>['build', 'linux'] + ); + expect(fs.file('linux/flutter/generated_config').existsSync(), true); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => mockProcessManager, + Platform: () => linuxPlatform, + }); + + testUsingContext('linux can extract binary name from Makefile', () async { + fs.file('linux/Makefile') + ..createSync(recursive: true) + ..writeAsStringSync(r''' +# Comment +SOMETHING_ELSE=FOO +BINARY_NAME=fizz_bar +'''); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + final FlutterProject flutterProject = FlutterProject.current(); + + expect(makefileExecutableName(flutterProject.linux), 'fizz_bar'); + }, overrides: <Type, Generator>{FileSystem: () => MemoryFileSystem()}); +} + +class MockProcessManager extends Mock implements ProcessManager {} +class MockProcess extends Mock implements Process {} +class MockPlatform extends Mock implements Platform { + @override + Map<String, String> environment = <String, String>{ + 'FLUTTER_ROOT': '/', + }; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart new file mode 100644 index 0000000..ceb07b8 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart
@@ -0,0 +1,117 @@ +// Copyright 2019 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. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/build.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + MockProcessManager mockProcessManager; + MemoryFileSystem memoryFilesystem; + MockProcess mockProcess; + MockPlatform macosPlatform; + MockPlatform notMacosPlatform; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockProcessManager = MockProcessManager(); + memoryFilesystem = MemoryFileSystem(); + mockProcess = MockProcess(); + macosPlatform = MockPlatform(); + notMacosPlatform = MockPlatform(); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { + return 0; + }); + when(mockProcess.stderr).thenAnswer((Invocation invocation) { + return const Stream<List<int>>.empty(); + }); + when(mockProcess.stdout).thenAnswer((Invocation invocation) { + return const Stream<List<int>>.empty(); + }); + when(macosPlatform.isMacOS).thenReturn(true); + when(notMacosPlatform.isMacOS).thenReturn(false); + }); + + testUsingContext('macOS build fails when there is no macos project', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + expect(createTestCommandRunner(command).run( + const <String>['build', 'macos'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => macosPlatform, + }); + + testUsingContext('macOS build fails on non-macOS platform', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + + expect(createTestCommandRunner(command).run( + const <String>['build', 'macos'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => notMacosPlatform, + FileSystem: () => memoryFilesystem, + }); + + testUsingContext('macOS build invokes build script', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.directory('macos').createSync(); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.currentDirectory); + final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory()); + + when(mockProcessManager.start(<String>[ + '/usr/bin/env', + 'xcrun', + 'xcodebuild', + '-workspace', flutterProject.macos.xcodeWorkspace.path, + '-configuration', 'Debug', + '-scheme', 'Runner', + '-derivedDataPath', flutterBuildDir.absolute.path, + 'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', + 'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', + ], runInShell: true)).thenAnswer((Invocation invocation) async { + return mockProcess; + }); + + await createTestCommandRunner(command).run( + const <String>['build', 'macos'] + ); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFilesystem, + ProcessManager: () => mockProcessManager, + Platform: () => macosPlatform, + }); +} + +class MockProcessManager extends Mock implements ProcessManager {} +class MockProcess extends Mock implements Process {} +class MockPlatform extends Mock implements Platform { + @override + Map<String, String> environment = <String, String>{ + 'FLUTTER_ROOT': '/', + }; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_web_test.dart b/packages/flutter_tools/test/general.shard/commands/build_web_test.dart new file mode 100644 index 0000000..2adb818 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_web_test.dart
@@ -0,0 +1,97 @@ +// Copyright 2019 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. + +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/resident_web_runner.dart'; +import 'package:flutter_tools/src/version.dart'; +import 'package:flutter_tools/src/web/compile.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + +void main() { + MockWebCompilationProxy mockWebCompilationProxy; + Testbed testbed; + MockPlatform mockPlatform; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockWebCompilationProxy = MockWebCompilationProxy(); + testbed = Testbed(setup: () { + fs.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync('name: foo\n'); + fs.file('.packages').createSync(); + fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + when(mockWebCompilationProxy.initialize( + projectDirectory: anyNamed('projectDirectory'), + release: anyNamed('release') + )).thenAnswer((Invocation invocation) { + final String path = fs.path.join('.dart_tool', 'build', 'flutter_web', 'foo', 'lib', 'main_web_entrypoint.dart.js'); + fs.file(path).createSync(recursive: true); + fs.file('$path.map').createSync(); + return Future<bool>.value(true); + }); + }, overrides: <Type, Generator>{ + WebCompilationProxy: () => mockWebCompilationProxy, + Platform: () => mockPlatform, + FlutterVersion: () => MockFlutterVersion(), + }); + }); + + test('Refuses to build for web when missing index.html', () => testbed.run(() async { + fs.file(fs.path.join('web', 'index.html')).deleteSync(); + + expect(buildWeb( + FlutterProject.current(), + fs.path.join('lib', 'main.dart'), + BuildInfo.debug, + ), throwsA(isInstanceOf<ToolExit>())); + })); + + test('Refuses to build using runner when missing index.html', () => testbed.run(() async { + fs.file(fs.path.join('web', 'index.html')).deleteSync(); + + final ResidentWebRunner runner = ResidentWebRunner( + <FlutterDevice>[], + flutterProject: FlutterProject.current(), + ipv6: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + expect(await runner.run(), 1); + })); + + test('Can build for web', () => testbed.run(() async { + + await buildWeb( + FlutterProject.current(), + fs.path.join('lib', 'main.dart'), + BuildInfo.debug, + ); + })); +} + +class MockWebCompilationProxy extends Mock implements WebCompilationProxy {} +class MockPlatform extends Mock implements Platform { + @override + Map<String, String> environment = <String, String>{ + 'FLUTTER_ROOT': '/', + }; +} +class MockFlutterVersion extends Mock implements FlutterVersion { + @override + bool get isMaster => true; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_windows_test.dart b/packages/flutter_tools/test/general.shard/commands/build_windows_test.dart new file mode 100644 index 0000000..301fa7d --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_windows_test.dart
@@ -0,0 +1,146 @@ +// Copyright 2019 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. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/build.dart'; +import 'package:flutter_tools/src/windows/visual_studio.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; +import 'package:xml/xml.dart' as xml; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + MockProcessManager mockProcessManager; + MemoryFileSystem memoryFilesystem; + MockProcess mockProcess; + MockPlatform windowsPlatform; + MockPlatform notWindowsPlatform; + MockVisualStudio mockVisualStudio; + const String solutionPath = r'C:\windows\Runner.sln'; + const String visualStudioPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community'; + const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat'; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockProcessManager = MockProcessManager(); + memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockProcess = MockProcess(); + windowsPlatform = MockPlatform() + ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\'; + notWindowsPlatform = MockPlatform(); + mockVisualStudio = MockVisualStudio(); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { + return 0; + }); + when(mockProcess.stderr).thenAnswer((Invocation invocation) { + return const Stream<List<int>>.empty(); + }); + when(mockProcess.stdout).thenAnswer((Invocation invocation) { + return const Stream<List<int>>.empty(); + }); + when(windowsPlatform.isWindows).thenReturn(true); + when(notWindowsPlatform.isWindows).thenReturn(false); + }); + + testUsingContext('Windows build fails when there is no vcvars64.bat', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.file(solutionPath).createSync(recursive: true); + expect(createTestCommandRunner(command).run( + const <String>['build', 'windows'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => windowsPlatform, + FileSystem: () => memoryFilesystem, + VisualStudio: () => mockVisualStudio, + }); + + testUsingContext('Windows build fails when there is no windows project', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath); + expect(createTestCommandRunner(command).run( + const <String>['build', 'windows'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => windowsPlatform, + FileSystem: () => memoryFilesystem, + VisualStudio: () => mockVisualStudio, + }); + + testUsingContext('Windows build fails on non windows platform', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.file(solutionPath).createSync(recursive: true); + when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + + expect(createTestCommandRunner(command).run( + const <String>['build', 'windows'] + ), throwsA(isInstanceOf<ToolExit>())); + }, overrides: <Type, Generator>{ + Platform: () => notWindowsPlatform, + FileSystem: () => memoryFilesystem, + VisualStudio: () => mockVisualStudio, + }); + + testUsingContext('Windows build invokes msbuild and writes generated files', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.file(solutionPath).createSync(recursive: true); + when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + + when(mockProcessManager.start(<String>[ + r'C:\packages\flutter_tools\bin\vs_build.bat', + vcvarsPath, + fs.path.basename(solutionPath), + 'Release', + ], workingDirectory: fs.path.dirname(solutionPath))).thenAnswer((Invocation invocation) async { + return mockProcess; + }); + + await createTestCommandRunner(command).run( + const <String>['build', 'windows'] + ); + + // Spot-check important elements from the properties file. + final File propsFile = fs.file(r'C:\windows\flutter\Generated.props'); + expect(propsFile.existsSync(), true); + final xml.XmlDocument props = xml.parse(propsFile.readAsStringSync()); + expect(props.findAllElements('PropertyGroup').first.getAttribute('Label'), 'UserMacros'); + expect(props.findAllElements('ItemGroup').length, 1); + expect(props.findAllElements('FLUTTER_ROOT').first.text, r'C:\'); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFilesystem, + ProcessManager: () => mockProcessManager, + Platform: () => windowsPlatform, + VisualStudio: () => mockVisualStudio, + }); +} + +class MockProcessManager extends Mock implements ProcessManager {} +class MockProcess extends Mock implements Process {} +class MockPlatform extends Mock implements Platform { + @override + Map<String, String> environment = <String, String>{ + 'FLUTTER_ROOT': r'C:\', + }; +} +class MockVisualStudio extends Mock implements VisualStudio {}
diff --git a/packages/flutter_tools/test/general.shard/commands/clean_test.dart b/packages/flutter_tools/test/general.shard/commands/clean_test.dart new file mode 100644 index 0000000..85926c7 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/clean_test.dart
@@ -0,0 +1,51 @@ +// Copyright 2019 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. + +import 'package:flutter_tools/src/base/config.dart'; +import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/commands/clean.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + + +void main() { + final MockFileSystem mockFileSystem = MockFileSystem(); + final MockDirectory currentDirectory = MockDirectory(); + final MockDirectory exampleDirectory = MockDirectory(); + final MockDirectory buildDirectory = MockDirectory(); + final MockDirectory dartToolDirectory = MockDirectory(); + final MockFile pubspec = MockFile(); + final MockFile examplePubspec = MockFile(); + + when(mockFileSystem.currentDirectory).thenReturn(currentDirectory); + when(currentDirectory.childDirectory('example')).thenReturn(exampleDirectory); + when(currentDirectory.childFile('pubspec.yaml')).thenReturn(pubspec); + when(pubspec.path).thenReturn('/test/pubspec.yaml'); + when(exampleDirectory.childFile('pubspec.yaml')).thenReturn(examplePubspec); + when(currentDirectory.childDirectory('.dart_tool')).thenReturn(dartToolDirectory); + when(examplePubspec.path).thenReturn('/test/example/pubspec.yaml'); + when(mockFileSystem.isFileSync('/test/pubspec.yaml')).thenReturn(false); + when(mockFileSystem.isFileSync('/test/example/pubspec.yaml')).thenReturn(false); + when(mockFileSystem.directory('build')).thenReturn(buildDirectory); + when(mockFileSystem.path).thenReturn(fs.path); + when(buildDirectory.existsSync()).thenReturn(true); + when(dartToolDirectory.existsSync()).thenReturn(true); + group(CleanCommand, () { + testUsingContext('removes build and .dart_tool directories', () async { + await CleanCommand().runCommand(); + verify(buildDirectory.deleteSync(recursive: true)).called(1); + verify(dartToolDirectory.deleteSync(recursive: true)).called(1); + }, overrides: <Type, Generator>{ + FileSystem: () => mockFileSystem, + Config: () => null, + }); + }); +} + +class MockFileSystem extends Mock implements FileSystem {} +class MockFile extends Mock implements File {} +class MockDirectory extends Mock implements Directory {}
diff --git a/packages/flutter_tools/test/general.shard/commands/config_test.dart b/packages/flutter_tools/test/general.shard/commands/config_test.dart new file mode 100644 index 0000000..b95b4ed --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/config_test.dart
@@ -0,0 +1,56 @@ +// Copyright 2016 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. + +import 'dart:convert'; + +import 'package:flutter_tools/src/android/android_sdk.dart'; +import 'package:flutter_tools/src/android/android_studio.dart'; +import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/commands/config.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + MockAndroidStudio mockAndroidStudio; + MockAndroidSdk mockAndroidSdk; + + setUp(() { + mockAndroidStudio = MockAndroidStudio(); + mockAndroidSdk = MockAndroidSdk(); + }); + + group('config', () { + testUsingContext('machine flag', () async { + final BufferLogger logger = context.get<Logger>(); + final ConfigCommand command = ConfigCommand(); + await command.handleMachine(); + + expect(logger.statusText, isNotEmpty); + final dynamic jsonObject = json.decode(logger.statusText); + expect(jsonObject, isMap); + + expect(jsonObject.containsKey('android-studio-dir'), true); + expect(jsonObject['android-studio-dir'], isNotNull); + + expect(jsonObject.containsKey('android-sdk'), true); + expect(jsonObject['android-sdk'], isNotNull); + }, overrides: <Type, Generator>{ + AndroidStudio: () => mockAndroidStudio, + AndroidSdk: () => mockAndroidSdk, + }); + }); +} + +class MockAndroidStudio extends Mock implements AndroidStudio, Comparable<AndroidStudio> { + @override + String get directory => 'path/to/android/stdio'; +} + +class MockAndroidSdk extends Mock implements AndroidSdk { + @override + String get directory => 'path/to/android/sdk'; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/create_test.dart b/packages/flutter_tools/test/general.shard/commands/create_test.dart new file mode 100644 index 0000000..7a8c087 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/create_test.dart
@@ -0,0 +1,1266 @@ +// Copyright 2015 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. + +// This test performs too poorly to run with coverage enabled. +@Tags(<String>['create', 'no_coverage']) +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/net.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/create.dart'; +import 'package:flutter_tools/src/dart/sdk.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/version.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + + +const String frameworkRevision = '12345678'; +const String frameworkChannel = 'omega'; +final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false; +final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{ + Platform: _kNoColorTerminalPlatform, +}; +const String samplesIndexJson = '''[ + { "id": "sample1" }, + { "id": "sample2" } +]'''; + +void main() { + Directory tempDir; + Directory projectDir; + FlutterVersion mockFlutterVersion; + LoggingProcessManager loggingProcessManager; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + loggingProcessManager = LoggingProcessManager(); + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.'); + projectDir = tempDir.childDirectory('flutter_project'); + mockFlutterVersion = MockFlutterVersion(); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + // Verify that we create a default project ('app') that is + // well-formed. + testUsingContext('can create a default project', () async { + await _createAndAnalyzeProject( + projectDir, + <String>[], + <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + 'flutter_project.iml', + 'ios/Flutter/AppFrameworkInfo.plist', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/GeneratedPluginRegistrant.h', + 'lib/main.dart', + ], + ); + return _runFlutterTest(projectDir); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('can create a default project if empty directory exists', () async { + await projectDir.create(recursive: true); + await _createAndAnalyzeProject( + projectDir, + <String>[], + <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + 'flutter_project.iml', + 'ios/Flutter/AppFrameworkInfo.plist', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/GeneratedPluginRegistrant.h', + ], + ); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('creates a module project correctly', () async { + await _createAndAnalyzeProject(projectDir, <String>[ + '--template=module', + ], <String>[ + '.android/app/', + '.gitignore', + '.ios/Flutter', + '.metadata', + 'lib/main.dart', + 'pubspec.yaml', + 'README.md', + 'test/widget_test.dart', + ], unexpectedPaths: <String>[ + 'android/', + 'ios/', + ]); + return _runFlutterTest(projectDir); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('cannot create a project if non-empty non-project directory exists with .metadata', () async { + await projectDir.absolute.childDirectory('blag').create(recursive: true); + await projectDir.absolute.childFile('.metadata').writeAsString('project_type: blag\n'); + expect( + () async => await _createAndAnalyzeProject(projectDir, <String>[], <String>[], unexpectedPaths: <String>[ + 'android/', + 'ios/', + '.android/', + '.ios/', + ]), + throwsToolExit(message: 'Sorry, unable to detect the type of project to recreate')); + }, timeout: allowForRemotePubInvocation, overrides: noColorTerminalOverride); + + testUsingContext('Will create an app project if non-empty non-project directory exists without .metadata', () async { + await projectDir.absolute.childDirectory('blag').create(recursive: true); + await projectDir.absolute.childDirectory('.idea').create(recursive: true); + await _createAndAnalyzeProject(projectDir, <String>[], <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + 'flutter_project.iml', + 'ios/Flutter/AppFrameworkInfo.plist', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/GeneratedPluginRegistrant.h', + ], unexpectedPaths: <String>[ + '.android/', + '.ios/', + ]); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('detects and recreates an app project correctly', () async { + await projectDir.absolute.childDirectory('lib').create(recursive: true); + await projectDir.absolute.childDirectory('ios').create(recursive: true); + await _createAndAnalyzeProject(projectDir, <String>[], <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + 'flutter_project.iml', + 'ios/Flutter/AppFrameworkInfo.plist', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/GeneratedPluginRegistrant.h', + ], unexpectedPaths: <String>[ + '.android/', + '.ios/', + ]); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('detects and recreates a plugin project correctly', () async { + await projectDir.create(recursive: true); + await projectDir.absolute.childFile('.metadata').writeAsString('project_type: plugin\n'); + return _createAndAnalyzeProject( + projectDir, + <String>[], + <String>[ + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'example/ios/Runner/AppDelegate.h', + 'example/ios/Runner/AppDelegate.m', + 'example/ios/Runner/main.m', + 'example/lib/main.dart', + 'flutter_project.iml', + 'ios/Classes/FlutterProjectPlugin.h', + 'ios/Classes/FlutterProjectPlugin.m', + 'lib/flutter_project.dart', + ], + ); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('detects and recreates a package project correctly', () async { + await projectDir.create(recursive: true); + await projectDir.absolute.childFile('.metadata').writeAsString('project_type: package\n'); + return _createAndAnalyzeProject( + projectDir, + <String>[], + <String>[ + 'lib/flutter_project.dart', + 'test/flutter_project_test.dart', + ], + unexpectedPaths: <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'example/ios/Runner/AppDelegate.h', + 'example/ios/Runner/AppDelegate.m', + 'example/ios/Runner/main.m', + 'example/lib/main.dart', + 'ios/Classes/FlutterProjectPlugin.h', + 'ios/Classes/FlutterProjectPlugin.m', + 'ios/Runner/AppDelegate.h', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/main.m', + 'lib/main.dart', + 'test/widget_test.dart', + ], + ); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('kotlin/swift legacy app project', () async { + return _createProject( + projectDir, + <String>['--no-pub', '--template=app', '--android-language=kotlin', '--ios-language=swift'], + <String>[ + 'android/app/src/main/kotlin/com/example/flutter_project/MainActivity.kt', + 'ios/Runner/AppDelegate.swift', + 'ios/Runner/Runner-Bridging-Header.h', + 'lib/main.dart', + '.idea/libraries/KotlinJavaRuntime.xml', + ], + unexpectedPaths: <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'ios/Runner/AppDelegate.h', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/main.m', + ], + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can create a package project', () async { + await _createAndAnalyzeProject( + projectDir, + <String>['--template=package'], + <String>[ + 'lib/flutter_project.dart', + 'test/flutter_project_test.dart', + ], + unexpectedPaths: <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'example/ios/Runner/AppDelegate.h', + 'example/ios/Runner/AppDelegate.m', + 'example/ios/Runner/main.m', + 'example/lib/main.dart', + 'ios/Classes/FlutterProjectPlugin.h', + 'ios/Classes/FlutterProjectPlugin.m', + 'ios/Runner/AppDelegate.h', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/main.m', + 'lib/main.dart', + 'test/widget_test.dart', + ], + ); + return _runFlutterTest(projectDir); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('can create a plugin project', () async { + await _createAndAnalyzeProject( + projectDir, + <String>['--template=plugin'], + <String>[ + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'example/ios/Runner/AppDelegate.h', + 'example/ios/Runner/AppDelegate.m', + 'example/ios/Runner/main.m', + 'example/lib/main.dart', + 'flutter_project.iml', + 'ios/Classes/FlutterProjectPlugin.h', + 'ios/Classes/FlutterProjectPlugin.m', + 'lib/flutter_project.dart', + ], + ); + return _runFlutterTest(projectDir.childDirectory('example')); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('kotlin/swift plugin project', () async { + return _createProject( + projectDir, + <String>['--no-pub', '--template=plugin', '-a', 'kotlin', '--ios-language', 'swift'], + <String>[ + 'android/src/main/kotlin/com/example/flutter_project/FlutterProjectPlugin.kt', + 'example/android/app/src/main/kotlin/com/example/flutter_project_example/MainActivity.kt', + 'example/ios/Runner/AppDelegate.swift', + 'example/ios/Runner/Runner-Bridging-Header.h', + 'example/lib/main.dart', + 'ios/Classes/FlutterProjectPlugin.h', + 'ios/Classes/FlutterProjectPlugin.m', + 'ios/Classes/SwiftFlutterProjectPlugin.swift', + 'lib/flutter_project.dart', + ], + unexpectedPaths: <String>[ + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'example/ios/Runner/AppDelegate.h', + 'example/ios/Runner/AppDelegate.m', + 'example/ios/Runner/main.m', + ], + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('plugin project with custom org', () async { + return _createProject( + projectDir, + <String>['--no-pub', '--template=plugin', '--org', 'com.bar.foo'], + <String>[ + 'android/src/main/java/com/bar/foo/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java', + ], + unexpectedPaths: <String>[ + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + ], + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('plugin project with valid custom project name', () async { + return _createProject( + projectDir, + <String>['--no-pub', '--template=plugin', '--project-name', 'xyz'], + <String>[ + 'android/src/main/java/com/example/xyz/XyzPlugin.java', + 'example/android/app/src/main/java/com/example/xyz_example/MainActivity.java', + ], + unexpectedPaths: <String>[ + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + ], + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('plugin project with invalid custom project name', () async { + expect( + () => _createProject(projectDir, + <String>['--no-pub', '--template=plugin', '--project-name', 'xyz.xyz'], + <String>[], + ), + throwsToolExit(message: '"xyz.xyz" is not a valid Dart package name.'), + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('legacy app project with-driver-test', () async { + return _createAndAnalyzeProject( + projectDir, + <String>['--with-driver-test', '--template=app'], + <String>['lib/main.dart'], + ); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('module project with pub', () async { + return _createProject(projectDir, <String>[ + '--template=module', + ], <String>[ + '.android/build.gradle', + '.android/Flutter/build.gradle', + '.android/Flutter/src/main/AndroidManifest.xml', + '.android/Flutter/src/main/java/io/flutter/facade/Flutter.java', + '.android/Flutter/src/main/java/io/flutter/facade/FlutterFragment.java', + '.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + '.android/gradle.properties', + '.android/gradle/wrapper/gradle-wrapper.jar', + '.android/gradle/wrapper/gradle-wrapper.properties', + '.android/gradlew', + '.android/gradlew.bat', + '.android/include_flutter.groovy', + '.android/local.properties', + '.android/settings.gradle', + '.gitignore', + '.metadata', + '.packages', + 'lib/main.dart', + 'pubspec.lock', + 'pubspec.yaml', + 'README.md', + 'test/widget_test.dart', + ], unexpectedPaths: <String>[ + 'android/', + 'ios/', + ]); + }, timeout: allowForRemotePubInvocation); + + + testUsingContext('androidx app project', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--androidx', projectDir.path]); + + void expectExists(String relPath) { + expect(fs.isFileSync('${projectDir.path}/$relPath'), true); + } + + expectExists('android/gradle.properties'); + + final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString(); + + expect(actualContents.contains('useAndroidX'), true); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('non androidx app project', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--no-androidx', projectDir.path]); + + void expectExists(String relPath) { + expect(fs.isFileSync('${projectDir.path}/$relPath'), true); + } + + expectExists('android/gradle.properties'); + + final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString(); + + expect(actualContents.contains('useAndroidX'), false); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('androidx app module', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--template=module', '--no-pub', '--androidx', projectDir.path]); + + final FlutterProject project = FlutterProject.fromDirectory(projectDir); + expect( + project.usesAndroidX, + true, + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('non androidx app module', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--template=module', '--no-pub', '--no-androidx', projectDir.path]); + + final FlutterProject project = FlutterProject.fromDirectory(projectDir); + expect( + project.usesAndroidX, + false, + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('androidx plugin project', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=plugin', '--androidx', projectDir.path]); + + void expectExists(String relPath) { + expect(fs.isFileSync('${projectDir.path}/$relPath'), true); + } + + expectExists('android/gradle.properties'); + + final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString(); + + expect(actualContents.contains('useAndroidX'), true); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('non androidx plugin project', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=plugin', '--no-androidx', projectDir.path]); + + void expectExists(String relPath) { + expect(fs.isFileSync('${projectDir.path}/$relPath'), true); + } + + expectExists('android/gradle.properties'); + + final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString(); + + expect(actualContents.contains('useAndroidX'), false); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('has correct content and formatting with module template', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--template=module', '--no-pub', '--org', 'com.foo.bar', projectDir.path]); + + void expectExists(String relPath, [bool expectation = true]) { + expect(fs.isFileSync('${projectDir.path}/$relPath'), expectation); + } + + expectExists('lib/main.dart'); + expectExists('test/widget_test.dart'); + + final String actualContents = await fs.file(projectDir.path + '/test/widget_test.dart').readAsString(); + + expect(actualContents.contains('flutter_test.dart'), true); + + for (FileSystemEntity file in projectDir.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.dart')) { + final String original = file.readAsStringSync(); + + final Process process = await Process.start( + sdkBinaryName('dartfmt'), + <String>[file.path], + workingDirectory: projectDir.path, + ); + final String formatted = await process.stdout.transform(utf8.decoder).join(); + + expect(original, formatted, reason: file.path); + } + } + + await _runFlutterTest(projectDir, target: fs.path.join(projectDir.path, 'test', 'widget_test.dart')); + + // Generated Xcode settings + final String xcodeConfigPath = fs.path.join('.ios', 'Flutter', 'Generated.xcconfig'); + expectExists(xcodeConfigPath); + final File xcodeConfigFile = fs.file(fs.path.join(projectDir.path, xcodeConfigPath)); + final String xcodeConfig = xcodeConfigFile.readAsStringSync(); + expect(xcodeConfig, contains('FLUTTER_ROOT=')); + expect(xcodeConfig, contains('FLUTTER_APPLICATION_PATH=')); + expect(xcodeConfig, contains('FLUTTER_TARGET=')); + // App identification + final String xcodeProjectPath = fs.path.join('.ios', 'Runner.xcodeproj', 'project.pbxproj'); + expectExists(xcodeProjectPath); + final File xcodeProjectFile = fs.file(fs.path.join(projectDir.path, xcodeProjectPath)); + final String xcodeProject = xcodeProjectFile.readAsStringSync(); + expect(xcodeProject, contains('PRODUCT_BUNDLE_IDENTIFIER = com.foo.bar.flutterProject')); + // Xcode build system + final String xcodeWorkspaceSettingsPath = fs.path.join('.ios', 'Runner.xcworkspace', 'xcshareddata', 'WorkspaceSettings.xcsettings'); + expectExists(xcodeWorkspaceSettingsPath, false); + + final String versionPath = fs.path.join('.metadata'); + expectExists(versionPath); + final String version = fs.file(fs.path.join(projectDir.path, versionPath)).readAsStringSync(); + expect(version, contains('version:')); + expect(version, contains('revision: 12345678')); + expect(version, contains('channel: omega')); + + // IntelliJ metadata + final String intelliJSdkMetadataPath = fs.path.join('.idea', 'libraries', 'Dart_SDK.xml'); + expectExists(intelliJSdkMetadataPath); + final String sdkMetaContents = fs + .file(fs.path.join( + projectDir.path, + intelliJSdkMetadataPath, + )) + .readAsStringSync(); + expect(sdkMetaContents, contains('<root url="file:/')); + expect(sdkMetaContents, contains('/bin/cache/dart-sdk/lib/core"')); + }, overrides: <Type, Generator>{ + FlutterVersion: () => mockFlutterVersion, + Platform: _kNoColorTerminalPlatform, + }, timeout: allowForCreateFlutterProject); + + testUsingContext('has correct content and formatting with app template', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'com.foo.bar', projectDir.path]); + + void expectExists(String relPath) { + expect(fs.isFileSync('${projectDir.path}/$relPath'), true); + } + + expectExists('lib/main.dart'); + expectExists('test/widget_test.dart'); + + for (FileSystemEntity file in projectDir.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.dart')) { + final String original = file.readAsStringSync(); + + final Process process = await Process.start( + sdkBinaryName('dartfmt'), + <String>[file.path], + workingDirectory: projectDir.path, + ); + final String formatted = await process.stdout.transform(utf8.decoder).join(); + + expect(original, formatted, reason: file.path); + } + } + + await _runFlutterTest(projectDir, target: fs.path.join(projectDir.path, 'test', 'widget_test.dart')); + + // Generated Xcode settings + final String xcodeConfigPath = fs.path.join('ios', 'Flutter', 'Generated.xcconfig'); + expectExists(xcodeConfigPath); + final File xcodeConfigFile = fs.file(fs.path.join(projectDir.path, xcodeConfigPath)); + final String xcodeConfig = xcodeConfigFile.readAsStringSync(); + expect(xcodeConfig, contains('FLUTTER_ROOT=')); + expect(xcodeConfig, contains('FLUTTER_APPLICATION_PATH=')); + // App identification + final String xcodeProjectPath = fs.path.join('ios', 'Runner.xcodeproj', 'project.pbxproj'); + expectExists(xcodeProjectPath); + final File xcodeProjectFile = fs.file(fs.path.join(projectDir.path, xcodeProjectPath)); + final String xcodeProject = xcodeProjectFile.readAsStringSync(); + expect(xcodeProject, contains('PRODUCT_BUNDLE_IDENTIFIER = com.foo.bar.flutterProject')); + + final String versionPath = fs.path.join('.metadata'); + expectExists(versionPath); + final String version = fs.file(fs.path.join(projectDir.path, versionPath)).readAsStringSync(); + expect(version, contains('version:')); + expect(version, contains('revision: 12345678')); + expect(version, contains('channel: omega')); + + // IntelliJ metadata + final String intelliJSdkMetadataPath = fs.path.join('.idea', 'libraries', 'Dart_SDK.xml'); + expectExists(intelliJSdkMetadataPath); + final String sdkMetaContents = fs + .file(fs.path.join( + projectDir.path, + intelliJSdkMetadataPath, + )) + .readAsStringSync(); + expect(sdkMetaContents, contains('<root url="file:/')); + expect(sdkMetaContents, contains('/bin/cache/dart-sdk/lib/core"')); + }, overrides: <Type, Generator>{ + FlutterVersion: () => mockFlutterVersion, + Platform: _kNoColorTerminalPlatform, + }, timeout: allowForCreateFlutterProject); + + testUsingContext('has correct application id for android and bundle id for ios', () async { + Cache.flutterRoot = '../..'; + when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision); + when(mockFlutterVersion.channel).thenReturn(frameworkChannel); + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + String tmpProjectDir = fs.path.join(tempDir.path, 'hello_flutter'); + await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'com.example', tmpProjectDir]); + FlutterProject project = FlutterProject.fromDirectory(fs.directory(tmpProjectDir)); + expect( + project.ios.productBundleIdentifier, + 'com.example.helloFlutter', + ); + expect( + project.android.applicationId, + 'com.example.hello_flutter', + ); + + tmpProjectDir = fs.path.join(tempDir.path, 'test_abc'); + await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'abc^*.1#@', tmpProjectDir]); + project = FlutterProject.fromDirectory(fs.directory(tmpProjectDir)); + expect( + project.ios.productBundleIdentifier, + 'abc.1.testAbc', + ); + expect( + project.android.applicationId, + 'abc.u1.test_abc', + ); + + tmpProjectDir = fs.path.join(tempDir.path, 'flutter_project'); + await runner.run(<String>['create', '--template=app', '--no-pub', '--org', '#+^%', tmpProjectDir]); + project = FlutterProject.fromDirectory(fs.directory(tmpProjectDir)); + expect( + project.ios.productBundleIdentifier, + 'flutterProject.untitled', + ); + expect( + project.android.applicationId, + 'flutter_project.untitled', + ); + }, overrides: <Type, Generator>{ + FlutterVersion: () => mockFlutterVersion, + Platform: _kNoColorTerminalPlatform, + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen default template over existing project', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); + expect(metadata, contains('project_type: app\n')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen default template over existing app project with no metadta and detect the type', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]); + + // Remove the .metadata to simulate an older instantiation that didn't generate those. + fs.file(fs.path.join(projectDir.path, '.metadata')).deleteSync(); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); + expect(metadata, contains('project_type: app\n')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen app template over existing app project and detect the type', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); + expect(metadata, contains('project_type: app\n')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen template over existing module project and detect the type', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=module', projectDir.path]); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); + expect(metadata, contains('project_type: module\n')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen default template over existing plugin project and detect the type', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); + expect(metadata, contains('project_type: plugin')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen default template over existing package project and detect the type', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--no-pub', '--template=package', projectDir.path]); + + await runner.run(<String>['create', '--no-pub', projectDir.path]); + + final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); + expect(metadata, contains('project_type: package')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen module .android/ folder, reusing custom org', () async { + await _createProject( + projectDir, + <String>['--template=module', '--org', 'com.bar.foo'], + <String>[], + ); + projectDir.childDirectory('.android').deleteSync(recursive: true); + return _createProject( + projectDir, + <String>[], + <String>[ + '.android/app/src/main/java/com/bar/foo/flutter_project/host/MainActivity.java', + ], + ); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('can re-gen module .ios/ folder, reusing custom org', () async { + await _createProject( + projectDir, + <String>['--template=module', '--org', 'com.bar.foo'], + <String>[], + ); + projectDir.childDirectory('.ios').deleteSync(recursive: true); + await _createProject(projectDir, <String>[], <String>[]); + final FlutterProject project = FlutterProject.fromDirectory(projectDir); + expect( + project.ios.productBundleIdentifier, + 'com.bar.foo.flutterProject', + ); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('can re-gen app android/ folder, reusing custom org', () async { + await _createProject( + projectDir, + <String>['--no-pub', '--template=app', '--org', 'com.bar.foo'], + <String>[], + ); + projectDir.childDirectory('android').deleteSync(recursive: true); + return _createProject( + projectDir, + <String>['--no-pub'], + <String>[ + 'android/app/src/main/java/com/bar/foo/flutter_project/MainActivity.java', + ], + unexpectedPaths: <String>[ + 'android/app/src/main/java/com/example/flutter_project/MainActivity.java', + ], + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen app ios/ folder, reusing custom org', () async { + await _createProject( + projectDir, + <String>['--no-pub', '--template=app', '--org', 'com.bar.foo'], + <String>[], + ); + projectDir.childDirectory('ios').deleteSync(recursive: true); + await _createProject(projectDir, <String>['--no-pub'], <String>[]); + final FlutterProject project = FlutterProject.fromDirectory(projectDir); + expect( + project.ios.productBundleIdentifier, + 'com.bar.foo.flutterProject', + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org', () async { + await _createProject( + projectDir, + <String>['--no-pub', '--template=plugin', '--org', 'com.bar.foo'], + <String>[], + ); + projectDir.childDirectory('example').deleteSync(recursive: true); + projectDir.childDirectory('ios').deleteSync(recursive: true); + await _createProject( + projectDir, + <String>['--no-pub', '--template=plugin'], + <String>[ + 'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java', + 'ios/Classes/FlutterProjectPlugin.h', + ], + unexpectedPaths: <String>[ + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + ], + ); + final FlutterProject project = FlutterProject.fromDirectory(projectDir); + expect( + project.example.ios.productBundleIdentifier, + 'com.bar.foo.flutterProjectExample', + ); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('fails to re-gen without specified org when org is ambiguous', () async { + await _createProject( + projectDir, + <String>['--no-pub', '--template=app', '--org', 'com.bar.foo'], + <String>[], + ); + fs.directory(fs.path.join(projectDir.path, 'ios')).deleteSync(recursive: true); + await _createProject( + projectDir, + <String>['--no-pub', '--template=app', '--org', 'com.bar.baz'], + <String>[], + ); + expect( + () => _createProject(projectDir, <String>[], <String>[]), + throwsToolExit(message: 'Ambiguous organization'), + ); + }, timeout: allowForCreateFlutterProject); + + // Verify that we help the user correct an option ordering issue + testUsingContext('produces sensible error message', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + expect( + runner.run(<String>['create', projectDir.path, '--pub']), + throwsToolExit(exitCode: 2, message: 'Try moving --pub'), + ); + }); + + testUsingContext('fails when file exists where output directory should be', () async { + Cache.flutterRoot = '../..'; + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad')); + if (!existingFile.existsSync()) { + existingFile.createSync(recursive: true); + } + expect( + runner.run(<String>['create', existingFile.path]), + throwsToolExit(message: 'existing file'), + ); + }); + + testUsingContext('fails overwrite when file exists where output directory should be', () async { + Cache.flutterRoot = '../..'; + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad')); + if (!existingFile.existsSync()) { + existingFile.createSync(recursive: true); + } + expect( + runner.run(<String>['create', '--overwrite', existingFile.path]), + throwsToolExit(message: 'existing file'), + ); + }); + + testUsingContext('overwrites existing directory when requested', () async { + Cache.flutterRoot = '../..'; + final Directory existingDirectory = fs.directory(fs.path.join(projectDir.path, 'bad')); + if (!existingDirectory.existsSync()) { + existingDirectory.createSync(recursive: true); + } + final File existingFile = fs.file(fs.path.join(existingDirectory.path, 'lib', 'main.dart')); + existingFile.createSync(recursive: true); + await _createProject( + fs.directory(existingDirectory.path), + <String>['--overwrite'], + <String>[ + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + 'lib/main.dart', + 'ios/Flutter/AppFrameworkInfo.plist', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/GeneratedPluginRegistrant.h', + ], + ); + }); + + testUsingContext('fails when invalid package name', () async { + Cache.flutterRoot = '../..'; + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + expect( + runner.run(<String>['create', fs.path.join(projectDir.path, 'invalidName')]), + throwsToolExit(message: '"invalidName" is not a valid Dart package name.'), + ); + }); + + testUsingContext( + 'invokes pub offline when requested', + () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--pub', '--offline', projectDir.path]); + expect(loggingProcessManager.commands.first, contains(matches(r'dart-sdk[\\/]bin[\\/]pub'))); + expect(loggingProcessManager.commands.first, contains('--offline')); + }, + timeout: allowForCreateFlutterProject, + overrides: <Type, Generator>{ + ProcessManager: () => loggingProcessManager, + }, + ); + + testUsingContext( + 'invokes pub online when offline not requested', + () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--pub', projectDir.path]); + expect(loggingProcessManager.commands.first, contains(matches(r'dart-sdk[\\/]bin[\\/]pub'))); + expect(loggingProcessManager.commands.first, isNot(contains('--offline'))); + }, + timeout: allowForCreateFlutterProject, + overrides: <Type, Generator>{ + ProcessManager: () => loggingProcessManager, + }, + ); + + testUsingContext('can create a sample-based project', () async { + await _createAndAnalyzeProject( + projectDir, + <String>['--no-pub', '--sample=foo.bar.Baz'], + <String>[ + 'lib/main.dart', + 'flutter_project.iml', + 'android/app/src/main/AndroidManifest.xml', + 'ios/Flutter/AppFrameworkInfo.plist', + ], + unexpectedPaths: <String>['test'], + ); + expect(projectDir.childDirectory('lib').childFile('main.dart').readAsStringSync(), + contains('void main() {}')); + }, timeout: allowForRemotePubInvocation, overrides: <Type, Generator>{ + HttpClientFactory: () => () => MockHttpClient(200, result: 'void main() {}'), + }); + + testUsingContext('can write samples index to disk', () async { + final String outputFile = fs.path.join(tempDir.path, 'flutter_samples.json'); + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<String> args = <String>[ + 'create', + '--list-samples', + outputFile, + ]; + + await runner.run(args); + final File expectedFile = fs.file(outputFile); + expect(expectedFile.existsSync(), isTrue); + expect(expectedFile.readAsStringSync(), equals(samplesIndexJson)); + }, overrides: <Type, Generator>{ + HttpClientFactory: () => + () => MockHttpClient(200, result: samplesIndexJson), + }); + testUsingContext('provides an error to the user if samples json download fails', () async { + final String outputFile = fs.path.join(tempDir.path, 'flutter_samples.json'); + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<String> args = <String>[ + 'create', + '--list-samples', + outputFile, + ]; + + await expectLater(runner.run(args), throwsToolExit(exitCode: 2, message: 'Failed to write samples')); + expect(fs.file(outputFile).existsSync(), isFalse); + }, overrides: <Type, Generator>{ + HttpClientFactory: () => + () => MockHttpClient(404, result: 'not found'), + }); +} + + +Future<void> _createProject( + Directory dir, + List<String> createArgs, + List<String> expectedPaths, { + List<String> unexpectedPaths = const <String>[], +}) async { + Cache.flutterRoot = '../../..'; + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + await runner.run(<String>[ + 'create', + ...createArgs, + dir.path, + ]); + + bool pathExists(String path) { + final String fullPath = fs.path.join(dir.path, path); + return fs.typeSync(fullPath) != FileSystemEntityType.notFound; + } + + final List<String> failures = <String>[]; + for (String path in expectedPaths) { + if (!pathExists(path)) { + failures.add('Path "$path" does not exist.'); + } + } + for (String path in unexpectedPaths) { + if (pathExists(path)) { + failures.add('Path "$path" exists when it shouldn\'t.'); + } + } + expect(failures, isEmpty, reason: failures.join('\n')); +} + +Future<void> _createAndAnalyzeProject( + Directory dir, + List<String> createArgs, + List<String> expectedPaths, { + List<String> unexpectedPaths = const <String>[], +}) async { + await _createProject(dir, createArgs, expectedPaths, unexpectedPaths: unexpectedPaths); + await _analyzeProject(dir.path); +} + +Future<void> _analyzeProject(String workingDir) async { + final String flutterToolsPath = fs.path.absolute(fs.path.join( + 'bin', + 'flutter_tools.dart', + )); + + final List<String> args = <String>[ + ...dartVmFlags, + flutterToolsPath, + 'analyze', + ]; + + final ProcessResult exec = await Process.run( + '$dartSdkPath/bin/dart', + args, + workingDirectory: workingDir, + ); + if (exec.exitCode != 0) { + print(exec.stdout); + print(exec.stderr); + } + expect(exec.exitCode, 0); +} + +Future<void> _runFlutterTest(Directory workingDir, { String target }) async { + final String flutterToolsPath = fs.path.absolute(fs.path.join( + 'bin', + 'flutter_tools.dart', + )); + + // While flutter test does get packages, it doesn't write version + // files anymore. + await Process.run( + '$dartSdkPath/bin/dart', + <String>[ + ...dartVmFlags, + flutterToolsPath, + 'packages', + 'get', + ], + workingDirectory: workingDir.path, + ); + + final List<String> args = <String>[ + ...dartVmFlags, + flutterToolsPath, + 'test', + '--no-color', + if (target != null) target, + ]; + + final ProcessResult exec = await Process.run( + '$dartSdkPath/bin/dart', + args, + workingDirectory: workingDir.path, + ); + if (exec.exitCode != 0) { + print(exec.stdout); + print(exec.stderr); + } + expect(exec.exitCode, 0); +} + +class MockFlutterVersion extends Mock implements FlutterVersion {} + +/// A ProcessManager that invokes a real process manager, but keeps +/// track of all commands sent to it. +class LoggingProcessManager extends LocalProcessManager { + List<List<String>> commands = <List<String>>[]; + + @override + Future<Process> start( + List<dynamic> command, { + String workingDirectory, + Map<String, String> environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal, + }) { + commands.add(command); + return super.start( + command, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + mode: mode, + ); + } +} + +class MockHttpClient implements HttpClient { + MockHttpClient(this.statusCode, {this.result}); + + final int statusCode; + final String result; + + @override + Future<HttpClientRequest> getUrl(Uri url) async { + return MockHttpClientRequest(statusCode, result: result); + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw 'io.HttpClient - $invocation'; + } +} + +class MockHttpClientRequest implements HttpClientRequest { + MockHttpClientRequest(this.statusCode, {this.result}); + + final int statusCode; + final String result; + + @override + Future<HttpClientResponse> close() async { + return MockHttpClientResponse(statusCode, result: result); + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw 'io.HttpClientRequest - $invocation'; + } +} + +class MockHttpClientResponse implements HttpClientResponse { + MockHttpClientResponse(this.statusCode, {this.result}); + + @override + final int statusCode; + + final String result; + + @override + String get reasonPhrase => '<reason phrase>'; + + @override + HttpClientResponseCompressionState get compressionState { + return HttpClientResponseCompressionState.decompressed; + } + + @override + StreamSubscription<Uint8List> listen( + void onData(Uint8List event), { + Function onError, + void onDone(), + bool cancelOnError, + }) { + return Stream<Uint8List>.fromIterable(<Uint8List>[Uint8List.fromList(result.codeUnits)]) + .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + Future<dynamic> forEach(void Function(Uint8List element) action) { + action(Uint8List.fromList(result.codeUnits)); + return Future<void>.value(); + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw 'io.HttpClientResponse - $invocation'; + } +}
diff --git a/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart b/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart new file mode 100644 index 0000000..735cee5 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart
@@ -0,0 +1,104 @@ +// Copyright 2015 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. + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/create.dart'; +import 'package:flutter_tools/src/doctor.dart'; +import 'package:flutter_tools/src/usage.dart'; + +import '../../src/common.dart'; +import '../../src/testbed.dart'; + + +void main() { + group('usageValues', () { + Testbed testbed; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + testbed = Testbed(setup: () { + final List<String> paths = <String>[ + fs.path.join('flutter', 'packages', 'flutter', 'pubspec.yaml'), + fs.path.join('flutter', 'packages', 'flutter_driver', 'pubspec.yaml'), + fs.path.join('flutter', 'packages', 'flutter_test', 'pubspec.yaml'), + fs.path.join('flutter', 'bin', 'cache', 'artifacts', 'gradle_wrapper', 'wrapper'), + fs.path.join('usr', 'local', 'bin', 'adb'), + fs.path.join('Android', 'platform-tools', 'foo'), + ]; + for (String path in paths) { + fs.file(path).createSync(recursive: true); + } + }, overrides: <Type, Generator>{ + DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), + }); + }); + + test('set template type as usage value', () => testbed.run(() async { + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=module', 'testy']); + expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'module')); + + await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']); + expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'app')); + + await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=package', 'testy']); + expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'package')); + + await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=plugin', 'testy']); + expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'plugin')); + })); + + test('set iOS host language type as usage value', () => testbed.run(() async { + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']); + expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'objc')); + + await runner.run(<String>[ + 'create', + '--flutter-root=flutter', + '--no-pub', + '--template=app', + '--ios-language=swift', + 'testy', + ]); + expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'swift')); + + })); + + test('set Android host language type as usage value', () => testbed.run(() async { + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']); + expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'java')); + + await runner.run(<String>[ + 'create', + '--flutter-root=flutter', + '--no-pub', + '--template=app', + '--android-language=kotlin', + 'testy', + ]); + expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'kotlin')); + })); + }); +} + +class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider { + @override + List<DoctorValidator> get validators => <DoctorValidator>[]; + + @override + List<Workflow> get workflows => <Workflow>[]; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/daemon_test.dart b/packages/flutter_tools/test/general.shard/commands/daemon_test.dart new file mode 100644 index 0000000..54aebdb --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/daemon_test.dart
@@ -0,0 +1,323 @@ +// Copyright 2015 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. + +import 'dart:async'; + +import 'package:flutter_tools/src/android/android_workflow.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/commands/daemon.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart'; +import 'package:flutter_tools/src/globals.dart'; +import 'package:flutter_tools/src/ios/ios_workflow.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + Daemon daemon; + NotifyingLogger notifyingLogger; + + group('daemon', () { + setUp(() { + notifyingLogger = NotifyingLogger(); + }); + + tearDown(() { + if (daemon != null) + return daemon.shutdown(); + notifyingLogger.dispose(); + }); + + testUsingContext('daemon.version command should succeed', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['result'], isNotEmpty); + expect(response['result'] is String, true); + await responses.close(); + await commands.close(); + }); + + testUsingContext('printError should send daemon.logMessage event', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + printError('daemon.logMessage test'); + final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) { + return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'error'; + }); + expect(response['id'], isNull); + expect(response['event'], 'daemon.logMessage'); + final Map<String, String> logMessage = response['params'].cast<String, String>(); + expect(logMessage['level'], 'error'); + expect(logMessage['message'], 'daemon.logMessage test'); + await responses.close(); + await commands.close(); + }, overrides: <Type, Generator>{ + Logger: () => notifyingLogger, + }); + + testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async { + final StringBuffer buffer = StringBuffer(); + + await runZoned<Future<void>>(() async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + logToStdout: true, + ); + printStatus('daemon.logMessage test'); + // Service the event loop. + await Future<void>.value(); + }, zoneSpecification: ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) { + buffer.writeln(line); + })); + + expect(buffer.toString().trim(), 'daemon.logMessage test'); + }, overrides: <Type, Generator>{ + Logger: () => notifyingLogger, + }); + + testUsingContext('daemon.shutdown command should stop daemon', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.shutdown'}); + return daemon.onExit.then<void>((int code) async { + await commands.close(); + expect(code, 0); + }); + }); + + testUsingContext('app.restart without an appId should report an error', () async { + final DaemonCommand command = DaemonCommand(); + applyMocksToCommand(command); + + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + daemonCommand: command, + notifyingLogger: notifyingLogger, + ); + + commands.add(<String, dynamic>{'id': 0, 'method': 'app.restart'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['error'], contains('appId is required')); + await responses.close(); + await commands.close(); + }); + + testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async { + final DaemonCommand command = DaemonCommand(); + applyMocksToCommand(command); + + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + daemonCommand: command, + notifyingLogger: notifyingLogger, + ); + + commands.add(<String, dynamic>{ + 'id': 0, + 'method': 'app.callServiceExtension', + 'params': <String, String>{ + 'methodName': 'ext.flutter.debugPaint', + }, + }); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['error'], contains('appId is required')); + await responses.close(); + await commands.close(); + }); + + testUsingContext('app.stop without appId should report an error', () async { + final DaemonCommand command = DaemonCommand(); + applyMocksToCommand(command); + + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + daemonCommand: command, + notifyingLogger: notifyingLogger, + ); + + commands.add(<String, dynamic>{'id': 0, 'method': 'app.stop'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['error'], contains('appId is required')); + await responses.close(); + await commands.close(); + }); + + testUsingContext('device.getDevices should respond with list', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['result'], isList); + await responses.close(); + await commands.close(); + }); + + testUsingContext('device.getDevices reports available devices', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery(); + daemon.deviceDomain.addDeviceDiscoverer(discoverer); + discoverer.addDevice(MockAndroidDevice()); + commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + final dynamic result = response['result']; + expect(result, isList); + expect(result, isNotEmpty); + await responses.close(); + await commands.close(); + }); + + testUsingContext('should send device.added event when device is discovered', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + + final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery(); + daemon.deviceDomain.addDeviceDiscoverer(discoverer); + discoverer.addDevice(MockAndroidDevice()); + + return await responses.stream.skipWhile(_isConnectedEvent).first.then<void>((Map<String, dynamic> response) async { + expect(response['event'], 'device.added'); + expect(response['params'], isMap); + + final Map<String, dynamic> params = response['params']; + expect(params['platform'], isNotEmpty); // the mock device has a platform of 'android-arm' + + await responses.close(); + await commands.close(); + }); + }, overrides: <Type, Generator>{ + AndroidWorkflow: () => MockAndroidWorkflow(), + IOSWorkflow: () => MockIOSWorkflow(), + FuchsiaWorkflow: () => MockFuchsiaWorkflow(), + }); + + testUsingContext('emulator.launch without an emulatorId should report an error', () async { + final DaemonCommand command = DaemonCommand(); + applyMocksToCommand(command); + + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + daemonCommand: command, + notifyingLogger: notifyingLogger, + ); + + commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['error'], contains('emulatorId is required')); + await responses.close(); + await commands.close(); + }); + + testUsingContext('emulator.getEmulators should respond with list', () async { + final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); + final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); + daemon = Daemon( + commands.stream, + responses.add, + notifyingLogger: notifyingLogger, + ); + commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.getEmulators'}); + final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent); + expect(response['id'], 0); + expect(response['result'], isList); + await responses.close(); + await commands.close(); + }); + }); + + group('daemon serialization', () { + test('OperationResult', () { + expect( + jsonEncodeObject(OperationResult.ok), + '{"code":0,"message":""}', + ); + expect( + jsonEncodeObject(OperationResult(1, 'foo')), + '{"code":1,"message":"foo"}', + ); + }); + }); +} + +bool _notEvent(Map<String, dynamic> map) => map['event'] == null; + +bool _isConnectedEvent(Map<String, dynamic> map) => map['event'] == 'daemon.connected'; + +class MockFuchsiaWorkflow extends FuchsiaWorkflow { + MockFuchsiaWorkflow({ this.canListDevices = true }); + + @override + final bool canListDevices; +} + +class MockAndroidWorkflow extends AndroidWorkflow { + MockAndroidWorkflow({ this.canListDevices = true }); + + @override + final bool canListDevices; +} + +class MockIOSWorkflow extends IOSWorkflow { + MockIOSWorkflow({ this.canListDevices =true }); + + @override + final bool canListDevices; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/devices_test.dart b/packages/flutter_tools/test/general.shard/commands/devices_test.dart new file mode 100644 index 0000000..01bbb9a --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/devices_test.dart
@@ -0,0 +1,71 @@ +// Copyright 2015 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_tools/src/android/android_sdk.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/devices.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('devices', () { + setUpAll(() { + Cache.disableLocking(); + // TODO(jonahwilliams): adjust the individual tests so they do not + // depend on the host environment. + debugDisableWebAndDesktop = true; + }); + + testUsingContext('returns 0 when called', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(<String>['devices']); + }); + + testUsingContext('no error when no connected devices', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(<String>['devices']); + expect(testLogger.statusText, contains('No devices detected')); + }, overrides: <Type, Generator>{ + AndroidSdk: () => null, + DeviceManager: () => DeviceManager(), + ProcessManager: () => MockProcessManager(), + }); + }); +} + +class MockProcessManager extends Mock implements ProcessManager { + @override + Future<ProcessResult> run( + List<dynamic> command, { + String workingDirectory, + Map<String, String> environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding stdoutEncoding = systemEncoding, + Encoding stderrEncoding = systemEncoding, + }) async { + return ProcessResult(0, 0, '', ''); + } + + @override + ProcessResult runSync( + List<dynamic> command, { + String workingDirectory, + Map<String, String> environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding stdoutEncoding = systemEncoding, + Encoding stderrEncoding = systemEncoding, + }) { + return ProcessResult(0, 0, '', ''); + } +}
diff --git a/packages/flutter_tools/test/general.shard/commands/doctor_test.dart b/packages/flutter_tools/test/general.shard/commands/doctor_test.dart new file mode 100644 index 0000000..5aa2a89 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/doctor_test.dart
@@ -0,0 +1,831 @@ +// Copyright 2015 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. + +import 'dart:async'; + +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/base/user_messages.dart'; +import 'package:flutter_tools/src/doctor.dart'; +import 'package:flutter_tools/src/globals.dart'; +import 'package:flutter_tools/src/proxy_validator.dart'; +import 'package:flutter_tools/src/vscode/vscode.dart'; +import 'package:flutter_tools/src/vscode/vscode_validator.dart'; +import 'package:flutter_tools/src/usage.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +final Generator _kNoColorOutputPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false; +final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{ + Platform: _kNoColorOutputPlatform, +}; + +void main() { + MockProcessManager mockProcessManager; + + setUp(() { + mockProcessManager = MockProcessManager(); + }); + + group('doctor', () { + testUsingContext('intellij validator', () async { + const String installPath = '/path/to/intelliJ'; + final ValidationResult result = await IntelliJValidatorTestTarget('Test', installPath).validate(); + expect(result.type, ValidationType.partial); + expect(result.statusInfo, 'version test.test.test'); + expect(result.messages, hasLength(4)); + + ValidationMessage message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('IntelliJ ')); + expect(message.message, 'IntelliJ at $installPath'); + + message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('Dart ')); + expect(message.message, 'Dart plugin version 162.2485'); + + message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); + expect(message.message, contains('Flutter plugin version 0.1.3')); + expect(message.message, contains('recommended minimum version')); + }, overrides: noColorTerminalOverride); + + testUsingContext('vs code validator when both installed', () async { + final ValidationResult result = await VsCodeValidatorTestTargets.installedWithExtension.validate(); + expect(result.type, ValidationType.installed); + expect(result.statusInfo, 'version 1.2.3'); + expect(result.messages, hasLength(2)); + + ValidationMessage message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code ')); + expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}'); + + message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); + expect(message.message, 'Flutter extension version 4.5.6'); + expect(message.isError, isFalse); + }, overrides: noColorTerminalOverride); + + testUsingContext('vs code validator when 64bit installed', () async { + expect(VsCodeValidatorTestTargets.installedWithExtension64bit.title, 'VS Code, 64-bit edition'); + final ValidationResult result = await VsCodeValidatorTestTargets.installedWithExtension64bit.validate(); + expect(result.type, ValidationType.installed); + expect(result.statusInfo, 'version 1.2.3'); + expect(result.messages, hasLength(2)); + + ValidationMessage message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code ')); + expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}'); + + message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); + expect(message.message, 'Flutter extension version 4.5.6'); + }, overrides: noColorTerminalOverride); + + testUsingContext('vs code validator when extension missing', () async { + final ValidationResult result = await VsCodeValidatorTestTargets.installedWithoutExtension.validate(); + expect(result.type, ValidationType.partial); + expect(result.statusInfo, 'version 1.2.3'); + expect(result.messages, hasLength(2)); + + ValidationMessage message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code ')); + expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}'); + + message = result.messages + .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); + expect(message.message, startsWith('Flutter extension not installed')); + expect(message.isError, isTrue); + }, overrides: noColorTerminalOverride); + }); + + group('proxy validator', () { + testUsingContext('does not show if HTTP_PROXY is not set', () { + expect(ProxyValidator.shouldShow, isFalse); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform()..environment = <String, String>{}, + }); + + testUsingContext('does not show if HTTP_PROXY is only whitespace', () { + expect(ProxyValidator.shouldShow, isFalse); + }, overrides: <Type, Generator>{ + Platform: () => + FakePlatform()..environment = <String, String>{'HTTP_PROXY': ' '}, + }); + + testUsingContext('shows when HTTP_PROXY is set', () { + expect(ProxyValidator.shouldShow, isTrue); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform() + ..environment = <String, String>{'HTTP_PROXY': 'fakeproxy.local'}, + }); + + testUsingContext('shows when http_proxy is set', () { + expect(ProxyValidator.shouldShow, isTrue); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform() + ..environment = <String, String>{'http_proxy': 'fakeproxy.local'}, + }); + + testUsingContext('reports success when NO_PROXY is configured correctly', () async { + final ValidationResult results = await ProxyValidator().validate(); + final List<ValidationMessage> issues = results.messages + .where((ValidationMessage msg) => msg.isError || msg.isHint) + .toList(); + expect(issues, hasLength(0)); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform() + ..environment = <String, String>{ + 'HTTP_PROXY': 'fakeproxy.local', + 'NO_PROXY': 'localhost,127.0.0.1', + }, + }); + + testUsingContext('reports success when no_proxy is configured correctly', () async { + final ValidationResult results = await ProxyValidator().validate(); + final List<ValidationMessage> issues = results.messages + .where((ValidationMessage msg) => msg.isError || msg.isHint) + .toList(); + expect(issues, hasLength(0)); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform() + ..environment = <String, String>{ + 'http_proxy': 'fakeproxy.local', + 'no_proxy': 'localhost,127.0.0.1', + }, + }); + + testUsingContext('reports issues when NO_PROXY is missing localhost', () async { + final ValidationResult results = await ProxyValidator().validate(); + final List<ValidationMessage> issues = results.messages + .where((ValidationMessage msg) => msg.isError || msg.isHint) + .toList(); + expect(issues, isNot(hasLength(0))); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform() + ..environment = <String, String>{ + 'HTTP_PROXY': 'fakeproxy.local', + 'NO_PROXY': '127.0.0.1', + }, + }); + + testUsingContext('reports issues when NO_PROXY is missing 127.0.0.1', () async { + final ValidationResult results = await ProxyValidator().validate(); + final List<ValidationMessage> issues = results.messages + .where((ValidationMessage msg) => msg.isError || msg.isHint) + .toList(); + expect(issues, isNot(hasLength(0))); + }, overrides: <Type, Generator>{ + Platform: () => FakePlatform() + ..environment = <String, String>{ + 'HTTP_PROXY': 'fakeproxy.local', + 'NO_PROXY': 'localhost', + }, + }); + }); + + group('doctor with overridden validators', () { + testUsingContext('validate non-verbose output format for run without issues', () async { + expect(await doctor.diagnose(verbose: false), isTrue); + expect(testLogger.statusText, equals( + 'Doctor summary (to see all details, run flutter doctor -v):\n' + '[✓] Passing Validator (with statusInfo)\n' + '[✓] Another Passing Validator (with statusInfo)\n' + '[✓] Providing validators is fun (with statusInfo)\n' + '\n' + '• No issues found!\n' + )); + }, overrides: <Type, Generator>{ + DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), + Platform: _kNoColorOutputPlatform, + }); + }); + + group('doctor usage params', () { + Usage mockUsage; + + setUp(() { + mockUsage = MockUsage(); + when(mockUsage.isFirstRun).thenReturn(true); + }); + + testUsingContext('contains installed', () async { + await doctor.diagnose(verbose: false); + + expect( + verify(mockUsage.sendEvent('doctorResult.PassingValidator', captureAny)).captured, + <dynamic>['installed', 'installed', 'installed'], + ); + }, overrides: <Type, Generator>{ + DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), + Platform: _kNoColorOutputPlatform, + Usage: () => mockUsage, + }); + + testUsingContext('contains installed and partial', () async { + await FakePassingDoctor().diagnose(verbose: false); + + expect( + verify(mockUsage.sendEvent('doctorResult.PassingValidator', captureAny)).captured, + <dynamic>['installed', 'installed'], + ); + expect( + verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithHintsOnly', captureAny)).captured, + <dynamic>['partial'], + ); + expect( + verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithErrors', captureAny)).captured, + <dynamic>['partial'], + ); + }, overrides: <Type, Generator>{ + Platform: _kNoColorOutputPlatform, + Usage: () => mockUsage, + }); + + testUsingContext('contains installed, missing and partial', () async { + await FakeDoctor().diagnose(verbose: false); + + expect( + verify(mockUsage.sendEvent('doctorResult.PassingValidator', captureAny)).captured, + <dynamic>['installed'], + ); + expect( + verify(mockUsage.sendEvent('doctorResult.MissingValidator', captureAny)).captured, + <dynamic>['missing'], + ); + expect( + verify(mockUsage.sendEvent('doctorResult.NotAvailableValidator', captureAny)).captured, + <dynamic>['notAvailable'], + ); + expect( + verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithHintsOnly', captureAny)).captured, + <dynamic>['partial'], + ); + expect( + verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithErrors', captureAny)).captured, + <dynamic>['partial'], + ); + }, overrides: <Type, Generator>{ + Platform: _kNoColorOutputPlatform, + Usage: () => mockUsage, + }); + }); + + group('doctor with fake validators', () { + testUsingContext('validate non-verbose output format for run without issues', () async { + expect(await FakeQuietDoctor().diagnose(verbose: false), isTrue); + expect(testLogger.statusText, equals( + 'Doctor summary (to see all details, run flutter doctor -v):\n' + '[✓] Passing Validator (with statusInfo)\n' + '[✓] Another Passing Validator (with statusInfo)\n' + '[✓] Validators are fun (with statusInfo)\n' + '[✓] Four score and seven validators ago (with statusInfo)\n' + '\n' + '• No issues found!\n' + )); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate non-verbose output format when only one category fails', () async { + expect(await FakeSinglePassingDoctor().diagnose(verbose: false), isTrue); + expect(testLogger.statusText, equals( + 'Doctor summary (to see all details, run flutter doctor -v):\n' + '[!] Partial Validator with only a Hint\n' + ' ! There is a hint here\n' + '\n' + '! Doctor found issues in 1 category.\n' + )); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate non-verbose output format for a passing run', () async { + expect(await FakePassingDoctor().diagnose(verbose: false), isTrue); + expect(testLogger.statusText, equals( + 'Doctor summary (to see all details, run flutter doctor -v):\n' + '[✓] Passing Validator (with statusInfo)\n' + '[!] Partial Validator with only a Hint\n' + ' ! There is a hint here\n' + '[!] Partial Validator with Errors\n' + ' ✗ An error message indicating partial installation\n' + ' ! Maybe a hint will help the user\n' + '[✓] Another Passing Validator (with statusInfo)\n' + '\n' + '! Doctor found issues in 2 categories.\n' + )); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate non-verbose output format', () async { + expect(await FakeDoctor().diagnose(verbose: false), isFalse); + expect(testLogger.statusText, equals( + 'Doctor summary (to see all details, run flutter doctor -v):\n' + '[✓] Passing Validator (with statusInfo)\n' + '[✗] Missing Validator\n' + ' ✗ A useful error message\n' + ' ! A hint message\n' + '[!] Not Available Validator\n' + ' ✗ A useful error message\n' + ' ! A hint message\n' + '[!] Partial Validator with only a Hint\n' + ' ! There is a hint here\n' + '[!] Partial Validator with Errors\n' + ' ✗ An error message indicating partial installation\n' + ' ! Maybe a hint will help the user\n' + '\n' + '! Doctor found issues in 4 categories.\n' + )); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate verbose output format', () async { + expect(await FakeDoctor().diagnose(verbose: true), isFalse); + expect(testLogger.statusText, equals( + '[✓] Passing Validator (with statusInfo)\n' + ' • A helpful message\n' + ' • A second, somewhat longer helpful message\n' + '\n' + '[✗] Missing Validator\n' + ' ✗ A useful error message\n' + ' • A message that is not an error\n' + ' ! A hint message\n' + '\n' + '[!] Not Available Validator\n' + ' ✗ A useful error message\n' + ' • A message that is not an error\n' + ' ! A hint message\n' + '\n' + '[!] Partial Validator with only a Hint\n' + ' ! There is a hint here\n' + ' • But there is no error\n' + '\n' + '[!] Partial Validator with Errors\n' + ' ✗ An error message indicating partial installation\n' + ' ! Maybe a hint will help the user\n' + ' • An extra message with some verbose details\n' + '\n' + '! Doctor found issues in 4 categories.\n' + )); + }, overrides: noColorTerminalOverride); + + testUsingContext('gen_snapshot does not work', () async { + when(mockProcessManager.runSync( + <String>[artifacts.getArtifactPath(Artifact.genSnapshot)], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenReturn(ProcessResult(101, 1, '', '')); + + expect(await FlutterValidatorDoctor().diagnose(verbose: false), isTrue); + final List<String> statusLines = testLogger.statusText.split('\n'); + for (String msg in userMessages.flutterBinariesDoNotRun.split('\n')) { + expect(statusLines, contains(contains(msg))); + } + if (platform.isLinux) { + for (String msg in userMessages.flutterBinariesLinuxRepairCommands.split('\n')) { + expect(statusLines, contains(contains(msg))); + } + } + }, overrides: <Type, Generator>{ + OutputPreferences: () => OutputPreferences(wrapText: false), + ProcessManager: () => mockProcessManager, + Platform: _kNoColorOutputPlatform, + }); + }); + + testUsingContext('validate non-verbose output wrapping', () async { + expect(await FakeDoctor().diagnose(verbose: false), isFalse); + expect(testLogger.statusText, equals( + 'Doctor summary (to see all\n' + 'details, run flutter doctor\n' + '-v):\n' + '[✓] Passing Validator (with\n' + ' statusInfo)\n' + '[✗] Missing Validator\n' + ' ✗ A useful error message\n' + ' ! A hint message\n' + '[!] Not Available Validator\n' + ' ✗ A useful error message\n' + ' ! A hint message\n' + '[!] Partial Validator with\n' + ' only a Hint\n' + ' ! There is a hint here\n' + '[!] Partial Validator with\n' + ' Errors\n' + ' ✗ An error message\n' + ' indicating partial\n' + ' installation\n' + ' ! Maybe a hint will help\n' + ' the user\n' + '\n' + '! Doctor found issues in 4\n' + ' categories.\n' + '' + )); + }, overrides: <Type, Generator>{ + OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 30), + Platform: _kNoColorOutputPlatform, + }); + + testUsingContext('validate verbose output wrapping', () async { + expect(await FakeDoctor().diagnose(verbose: true), isFalse); + expect(testLogger.statusText, equals( + '[✓] Passing Validator (with\n' + ' statusInfo)\n' + ' • A helpful message\n' + ' • A second, somewhat\n' + ' longer helpful message\n' + '\n' + '[✗] Missing Validator\n' + ' ✗ A useful error message\n' + ' • A message that is not an\n' + ' error\n' + ' ! A hint message\n' + '\n' + '[!] Not Available Validator\n' + ' ✗ A useful error message\n' + ' • A message that is not an\n' + ' error\n' + ' ! A hint message\n' + '\n' + '[!] Partial Validator with\n' + ' only a Hint\n' + ' ! There is a hint here\n' + ' • But there is no error\n' + '\n' + '[!] Partial Validator with\n' + ' Errors\n' + ' ✗ An error message\n' + ' indicating partial\n' + ' installation\n' + ' ! Maybe a hint will help\n' + ' the user\n' + ' • An extra message with\n' + ' some verbose details\n' + '\n' + '! Doctor found issues in 4\n' + ' categories.\n' + '' + )); + }, overrides: <Type, Generator>{ + OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 30), + Platform: _kNoColorOutputPlatform, + }); + + + group('doctor with grouped validators', () { + testUsingContext('validate diagnose combines validator output', () async { + expect(await FakeGroupedDoctor().diagnose(), isTrue); + expect(testLogger.statusText, equals( + '[✓] Category 1\n' + ' • A helpful message\n' + ' • A helpful message\n' + '\n' + '[!] Category 2\n' + ' • A helpful message\n' + ' ✗ A useful error message\n' + '\n' + '! Doctor found issues in 1 category.\n' + )); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate merging assigns statusInfo and title', () async { + // There are two subvalidators. Only the second contains statusInfo. + expect(await FakeGroupedDoctorWithStatus().diagnose(), isTrue); + expect(testLogger.statusText, equals( + '[✓] First validator title (A status message)\n' + ' • A helpful message\n' + ' • A different message\n' + '\n' + '• No issues found!\n' + )); + }, overrides: noColorTerminalOverride); + }); + + + group('grouped validator merging results', () { + final PassingGroupedValidator installed = PassingGroupedValidator('Category'); + final PartialGroupedValidator partial = PartialGroupedValidator('Category'); + final MissingGroupedValidator missing = MissingGroupedValidator('Category'); + + testUsingContext('validate installed + installed = installed', () async { + expect(await FakeSmallGroupDoctor(installed, installed).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[✓]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate installed + partial = partial', () async { + expect(await FakeSmallGroupDoctor(installed, partial).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate installed + missing = partial', () async { + expect(await FakeSmallGroupDoctor(installed, missing).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate partial + installed = partial', () async { + expect(await FakeSmallGroupDoctor(partial, installed).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate partial + partial = partial', () async { + expect(await FakeSmallGroupDoctor(partial, partial).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate partial + missing = partial', () async { + expect(await FakeSmallGroupDoctor(partial, missing).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate missing + installed = partial', () async { + expect(await FakeSmallGroupDoctor(missing, installed).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate missing + partial = partial', () async { + expect(await FakeSmallGroupDoctor(missing, partial).diagnose(), isTrue); + expect(testLogger.statusText, startsWith('[!]')); + }, overrides: noColorTerminalOverride); + + testUsingContext('validate missing + missing = missing', () async { + expect(await FakeSmallGroupDoctor(missing, missing).diagnose(), isFalse); + expect(testLogger.statusText, startsWith('[✗]')); + }, overrides: noColorTerminalOverride); + }); +} + +class MockUsage extends Mock implements Usage {} + +class IntelliJValidatorTestTarget extends IntelliJValidator { + IntelliJValidatorTestTarget(String title, String installPath) : super(title, installPath); + + @override + String get pluginsPath => fs.path.join('test', 'data', 'intellij', 'plugins'); + + @override + String get version => 'test.test.test'; +} + +class PassingValidator extends DoctorValidator { + PassingValidator(String name) : super(name); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage('A helpful message')); + messages.add(ValidationMessage('A second, somewhat longer helpful message')); + return ValidationResult(ValidationType.installed, messages, statusInfo: 'with statusInfo'); + } +} + +class MissingValidator extends DoctorValidator { + MissingValidator() : super('Missing Validator'); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage.error('A useful error message')); + messages.add(ValidationMessage('A message that is not an error')); + messages.add(ValidationMessage.hint('A hint message')); + return ValidationResult(ValidationType.missing, messages); + } +} + +class NotAvailableValidator extends DoctorValidator { + NotAvailableValidator() : super('Not Available Validator'); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage.error('A useful error message')); + messages.add(ValidationMessage('A message that is not an error')); + messages.add(ValidationMessage.hint('A hint message')); + return ValidationResult(ValidationType.notAvailable, messages); + } +} + +class PartialValidatorWithErrors extends DoctorValidator { + PartialValidatorWithErrors() : super('Partial Validator with Errors'); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage.error('An error message indicating partial installation')); + messages.add(ValidationMessage.hint('Maybe a hint will help the user')); + messages.add(ValidationMessage('An extra message with some verbose details')); + return ValidationResult(ValidationType.partial, messages); + } +} + +class PartialValidatorWithHintsOnly extends DoctorValidator { + PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint'); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage.hint('There is a hint here')); + messages.add(ValidationMessage('But there is no error')); + return ValidationResult(ValidationType.partial, messages); + } +} + +/// A doctor that fails with a missing [ValidationResult]. +class FakeDoctor extends Doctor { + List<DoctorValidator> _validators; + + @override + List<DoctorValidator> get validators { + if (_validators == null) { + _validators = <DoctorValidator>[]; + _validators.add(PassingValidator('Passing Validator')); + _validators.add(MissingValidator()); + _validators.add(NotAvailableValidator()); + _validators.add(PartialValidatorWithHintsOnly()); + _validators.add(PartialValidatorWithErrors()); + } + return _validators; + } +} + +/// A doctor that should pass, but still has issues in some categories. +class FakePassingDoctor extends Doctor { + List<DoctorValidator> _validators; + @override + List<DoctorValidator> get validators { + if (_validators == null) { + _validators = <DoctorValidator>[]; + _validators.add(PassingValidator('Passing Validator')); + _validators.add(PartialValidatorWithHintsOnly()); + _validators.add(PartialValidatorWithErrors()); + _validators.add(PassingValidator('Another Passing Validator')); + } + return _validators; + } +} + +/// A doctor that should pass, but still has 1 issue to test the singular of +/// categories. +class FakeSinglePassingDoctor extends Doctor { + List<DoctorValidator> _validators; + @override + List<DoctorValidator> get validators { + if (_validators == null) { + _validators = <DoctorValidator>[]; + _validators.add(PartialValidatorWithHintsOnly()); + } + return _validators; + } +} + +/// A doctor that passes and has no issues anywhere. +class FakeQuietDoctor extends Doctor { + List<DoctorValidator> _validators; + @override + List<DoctorValidator> get validators { + if (_validators == null) { + _validators = <DoctorValidator>[]; + _validators.add(PassingValidator('Passing Validator')); + _validators.add(PassingValidator('Another Passing Validator')); + _validators.add(PassingValidator('Validators are fun')); + _validators.add(PassingValidator('Four score and seven validators ago')); + } + return _validators; + } +} + +/// A DoctorValidatorsProvider that overrides the default validators without +/// overriding the doctor. +class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider { + @override + List<DoctorValidator> get validators { + return <DoctorValidator>[ + PassingValidator('Passing Validator'), + PassingValidator('Another Passing Validator'), + PassingValidator('Providing validators is fun'), + ]; + } + + @override + List<Workflow> get workflows => <Workflow>[]; +} + +class PassingGroupedValidator extends DoctorValidator { + PassingGroupedValidator(String name) : super(name); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage('A helpful message')); + return ValidationResult(ValidationType.installed, messages); + } +} + +class MissingGroupedValidator extends DoctorValidator { + MissingGroupedValidator(String name) : super(name); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage.error('A useful error message')); + return ValidationResult(ValidationType.missing, messages); + } +} + +class PartialGroupedValidator extends DoctorValidator { + PartialGroupedValidator(String name) : super(name); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage.error('An error message for partial installation')); + return ValidationResult(ValidationType.partial, messages); + } +} + +class PassingGroupedValidatorWithStatus extends DoctorValidator { + PassingGroupedValidatorWithStatus(String name) : super(name); + + @override + Future<ValidationResult> validate() async { + final List<ValidationMessage> messages = <ValidationMessage>[]; + messages.add(ValidationMessage('A different message')); + return ValidationResult(ValidationType.installed, messages, statusInfo: 'A status message'); + } +} + +/// A doctor that has two groups of two validators each. +class FakeGroupedDoctor extends Doctor { + List<DoctorValidator> _validators; + @override + List<DoctorValidator> get validators { + if (_validators == null) { + _validators = <DoctorValidator>[]; + _validators.add(GroupedValidator(<DoctorValidator>[ + PassingGroupedValidator('Category 1'), + PassingGroupedValidator('Category 1'), + ])); + _validators.add(GroupedValidator(<DoctorValidator>[ + PassingGroupedValidator('Category 2'), + MissingGroupedValidator('Category 2'), + ])); + } + return _validators; + } +} + +class FakeGroupedDoctorWithStatus extends Doctor { + List<DoctorValidator> _validators; + @override + List<DoctorValidator> get validators { + _validators ??= <DoctorValidator>[ + GroupedValidator(<DoctorValidator>[ + PassingGroupedValidator('First validator title'), + PassingGroupedValidatorWithStatus('Second validator title'), + ])]; + return _validators; + } +} + +class FlutterValidatorDoctor extends Doctor { + List<DoctorValidator> _validators; + @override + List<DoctorValidator> get validators { + _validators ??= <DoctorValidator>[FlutterValidator()]; + return _validators; + } +} + +/// A doctor that takes any two validators. Used to check behavior when +/// merging ValidationTypes (installed, missing, partial). +class FakeSmallGroupDoctor extends Doctor { + FakeSmallGroupDoctor(DoctorValidator val1, DoctorValidator val2) { + _validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])]; + } + + List<DoctorValidator> _validators; + + @override + List<DoctorValidator> get validators => _validators; +} + +class VsCodeValidatorTestTargets extends VsCodeValidator { + VsCodeValidatorTestTargets._(String installDirectory, String extensionDirectory, {String edition}) + : super(VsCode.fromDirectory(installDirectory, extensionDirectory, edition: edition)); + + static VsCodeValidatorTestTargets get installedWithExtension => + VsCodeValidatorTestTargets._(validInstall, validExtensions); + + static VsCodeValidatorTestTargets get installedWithExtension64bit => + VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition'); + + static VsCodeValidatorTestTargets get installedWithoutExtension => + VsCodeValidatorTestTargets._(validInstall, missingExtensions); + + static final String validInstall = fs.path.join('test', 'data', 'vscode', 'application'); + static final String validExtensions = fs.path.join('test', 'data', 'vscode', 'extensions'); + static final String missingExtensions = fs.path.join('test', 'data', 'vscode', 'notExtensions'); +} + +class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/commands/drive_test.dart b/packages/flutter_tools/test/general.shard/commands/drive_test.dart new file mode 100644 index 0000000..f6e39ed --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/drive_test.dart
@@ -0,0 +1,429 @@ +// Copyright 2016 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. + +import 'dart:async'; + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/android/android_device.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/drive.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('drive', () { + DriveCommand command; + Device mockDevice; + MemoryFileSystem fs; + Directory tempDir; + + void withMockDevice([ Device mock ]) { + mockDevice = mock ?? MockDevice(); + targetDeviceFinder = () async => mockDevice; + testDeviceManager.addDevice(mockDevice); + } + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + command = DriveCommand(); + applyMocksToCommand(command); + fs = MemoryFileSystem(); + tempDir = fs.systemTempDirectory.createTempSync('flutter_drive_test.'); + fs.currentDirectory = tempDir; + fs.directory('test').createSync(); + fs.directory('test_driver').createSync(); + fs.file('pubspec.yaml')..createSync(); + fs.file('.packages').createSync(); + setExitFunctionForTests(); + targetDeviceFinder = () { + throw 'Unexpected call to targetDeviceFinder'; + }; + appStarter = (DriveCommand command) { + throw 'Unexpected call to appStarter'; + }; + testRunner = (List<String> testArgs, String observatoryUri) { + throw 'Unexpected call to testRunner'; + }; + appStopper = (DriveCommand command) { + throw 'Unexpected call to appStopper'; + }; + }); + + tearDown(() { + command = null; + restoreExitFunction(); + restoreAppStarter(); + restoreAppStopper(); + restoreTestRunner(); + restoreTargetDeviceFinder(); + tryToDelete(tempDir); + }); + + testUsingContext('returns 1 when test file is not found', () async { + withMockDevice(); + + final String testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart'); + final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); + fs.file(testApp).createSync(recursive: true); + + final List<String> args = <String>[ + 'drive', + '--target=$testApp', + ]; + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode ?? 1, 1); + expect(e.message, contains('Test file not found: $testFile')); + } + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('returns 1 when app fails to run', () async { + withMockDevice(); + appStarter = expectAsync1((DriveCommand command) async => null); + + final String testApp = fs.path.join(tempDir.path, 'test_driver', 'e2e.dart'); + final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); + + final MemoryFileSystem memFs = fs; + await memFs.file(testApp).writeAsString('main() { }'); + await memFs.file(testFile).writeAsString('main() { }'); + + final List<String> args = <String>[ + 'drive', + '--target=$testApp', + ]; + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode, 1); + expect(e.message, contains('Application failed to start. Will not run test. Quitting.')); + } + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('returns 1 when app file is outside package', () async { + final String appFile = fs.path.join(tempDir.dirname, 'other_app', 'app.dart'); + fs.file(appFile).createSync(recursive: true); + final List<String> args = <String>[ + '--no-wrap', + 'drive', + '--target=$appFile', + ]; + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode ?? 1, 1); + expect(testLogger.errorText, contains( + 'Application file $appFile is outside the package directory ${tempDir.path}', + )); + } + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('returns 1 when app file is in the root dir', () async { + final String appFile = fs.path.join(tempDir.path, 'main.dart'); + fs.file(appFile).createSync(recursive: true); + final List<String> args = <String>[ + '--no-wrap', + 'drive', + '--target=$appFile', + ]; + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode ?? 1, 1); + expect(testLogger.errorText, contains( + 'Application file main.dart must reside in one of the ' + 'sub-directories of the package structure, not in the root directory.', + )); + } + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('returns 0 when test ends successfully', () async { + withMockDevice(); + + final String testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart'); + final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); + + appStarter = expectAsync1((DriveCommand command) async { + return LaunchResult.succeeded(); + }); + testRunner = expectAsync2((List<String> testArgs, String observatoryUri) async { + expect(testArgs, <String>[testFile]); + return null; + }); + appStopper = expectAsync1((DriveCommand command) async { + return true; + }); + + final MemoryFileSystem memFs = fs; + await memFs.file(testApp).writeAsString('main() {}'); + await memFs.file(testFile).writeAsString('main() {}'); + + final List<String> args = <String>[ + 'drive', + '--target=$testApp', + ]; + await createTestCommandRunner(command).run(args); + expect(testLogger.errorText, isEmpty); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('returns exitCode set by test runner', () async { + withMockDevice(); + + final String testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart'); + final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); + + appStarter = expectAsync1((DriveCommand command) async { + return LaunchResult.succeeded(); + }); + testRunner = (List<String> testArgs, String observatoryUri) async { + throwToolExit(null, exitCode: 123); + }; + appStopper = expectAsync1((DriveCommand command) async { + return true; + }); + + final MemoryFileSystem memFs = fs; + await memFs.file(testApp).writeAsString('main() {}'); + await memFs.file(testFile).writeAsString('main() {}'); + + final List<String> args = <String>[ + 'drive', + '--target=$testApp', + ]; + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode ?? 1, 123); + expect(e.message, isNull); + } + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + group('findTargetDevice', () { + testUsingContext('uses specified device', () async { + testDeviceManager.specifiedDeviceId = '123'; + withMockDevice(); + when(mockDevice.name).thenReturn('specified-device'); + when(mockDevice.id).thenReturn('123'); + + final Device device = await findTargetDevice(); + expect(device.name, 'specified-device'); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + }); + + void findTargetDeviceOnOperatingSystem(String operatingSystem) { + Platform platform() => FakePlatform(operatingSystem: operatingSystem); + + testUsingContext('returns null if no devices found', () async { + expect(await findTargetDevice(), isNull); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + Platform: platform, + }); + + testUsingContext('uses existing Android device', () async { + mockDevice = MockAndroidDevice(); + when(mockDevice.name).thenReturn('mock-android-device'); + withMockDevice(mockDevice); + + final Device device = await findTargetDevice(); + expect(device.name, 'mock-android-device'); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + Platform: platform, + }); + } + + group('findTargetDevice on Linux', () { + findTargetDeviceOnOperatingSystem('linux'); + }); + + group('findTargetDevice on Windows', () { + findTargetDeviceOnOperatingSystem('windows'); + }); + + group('findTargetDevice on macOS', () { + findTargetDeviceOnOperatingSystem('macos'); + + Platform macOsPlatform() => FakePlatform(operatingSystem: 'macos'); + + testUsingContext('uses existing simulator', () async { + withMockDevice(); + when(mockDevice.name).thenReturn('mock-simulator'); + when(mockDevice.isLocalEmulator) + .thenAnswer((Invocation invocation) => Future<bool>.value(true)); + + final Device device = await findTargetDevice(); + expect(device.name, 'mock-simulator'); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + Platform: macOsPlatform, + }); + }); + + group('build arguments', () { + String testApp, testFile; + + setUp(() { + restoreAppStarter(); + }); + + Future<void> appStarterSetup() async { + withMockDevice(); + + final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader(); + when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader); + final MockLaunchResult mockLaunchResult = MockLaunchResult(); + when(mockLaunchResult.started).thenReturn(true); + when(mockDevice.startApp( + null, + mainPath: anyNamed('mainPath'), + route: anyNamed('route'), + debuggingOptions: anyNamed('debuggingOptions'), + platformArgs: anyNamed('platformArgs'), + prebuiltApplication: anyNamed('prebuiltApplication'), + usesTerminalUi: false, + )).thenAnswer((_) => Future<LaunchResult>.value(mockLaunchResult)); + when(mockDevice.isAppInstalled(any)).thenAnswer((_) => Future<bool>.value(false)); + + testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart'); + testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); + + testRunner = (List<String> testArgs, String observatoryUri) async { + throwToolExit(null, exitCode: 123); + }; + appStopper = expectAsync1( + (DriveCommand command) async { + return true; + }, + count: 2, + ); + + final MemoryFileSystem memFs = fs; + await memFs.file(testApp).writeAsString('main() {}'); + await memFs.file(testFile).writeAsString('main() {}'); + } + + testUsingContext('does not use pre-built app if no build arg provided', () async { + await appStarterSetup(); + + final List<String> args = <String>[ + 'drive', + '--target=$testApp', + ]; + try { + await createTestCommandRunner(command).run(args); + } on ToolExit catch (e) { + expect(e.exitCode, 123); + expect(e.message, null); + } + verify(mockDevice.startApp( + null, + mainPath: anyNamed('mainPath'), + route: anyNamed('route'), + debuggingOptions: anyNamed('debuggingOptions'), + platformArgs: anyNamed('platformArgs'), + prebuiltApplication: false, + usesTerminalUi: false, + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('does not use pre-built app if --build arg provided', () async { + await appStarterSetup(); + + final List<String> args = <String>[ + 'drive', + '--build', + '--target=$testApp', + ]; + try { + await createTestCommandRunner(command).run(args); + } on ToolExit catch (e) { + expect(e.exitCode, 123); + expect(e.message, null); + } + verify(mockDevice.startApp( + null, + mainPath: anyNamed('mainPath'), + route: anyNamed('route'), + debuggingOptions: anyNamed('debuggingOptions'), + platformArgs: anyNamed('platformArgs'), + prebuiltApplication: false, + usesTerminalUi: false, + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('uses prebuilt app if --no-build arg provided', () async { + await appStarterSetup(); + + final List<String> args = <String>[ + 'drive', + '--no-build', + '--target=$testApp', + ]; + try { + await createTestCommandRunner(command).run(args); + } on ToolExit catch (e) { + expect(e.exitCode, 123); + expect(e.message, null); + } + verify(mockDevice.startApp( + null, + mainPath: anyNamed('mainPath'), + route: anyNamed('route'), + debuggingOptions: anyNamed('debuggingOptions'), + platformArgs: anyNamed('platformArgs'), + prebuiltApplication: true, + usesTerminalUi: false, + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + }); + }); +} + +class MockDevice extends Mock implements Device { + MockDevice() { + when(isSupported()).thenReturn(true); + } +} + +class MockAndroidDevice extends Mock implements AndroidDevice { } + +class MockLaunchResult extends Mock implements LaunchResult { }
diff --git a/packages/flutter_tools/test/general.shard/commands/format_test.dart b/packages/flutter_tools/test/general.shard/commands/format_test.dart new file mode 100644 index 0000000..1817bbe --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/format_test.dart
@@ -0,0 +1,78 @@ +// Copyright 2016 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. + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/format.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('format', () { + Directory tempDir; + + setUp(() { + Cache.disableLocking(); + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_format_test.'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + testUsingContext('a file', () async { + final String projectPath = await createProject(tempDir); + + final File srcFile = fs.file(fs.path.join(projectPath, 'lib', 'main.dart')); + final String original = srcFile.readAsStringSync(); + srcFile.writeAsStringSync(original.replaceFirst('main()', 'main( )')); + + final FormatCommand command = FormatCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + await runner.run(<String>['format', srcFile.path]); + + final String formatted = srcFile.readAsStringSync(); + expect(formatted, original); + }); + + testUsingContext('dry-run', () async { + final String projectPath = await createProject(tempDir); + + final File srcFile = fs.file( + fs.path.join(projectPath, 'lib', 'main.dart')); + final String nonFormatted = srcFile.readAsStringSync().replaceFirst( + 'main()', 'main( )'); + srcFile.writeAsStringSync(nonFormatted); + + final FormatCommand command = FormatCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + await runner.run(<String>['format', '--dry-run', srcFile.path]); + + final String shouldNotFormatted = srcFile.readAsStringSync(); + expect(shouldNotFormatted, nonFormatted); + }); + + testUsingContext('dry-run with set-exit-if-changed', () async { + final String projectPath = await createProject(tempDir); + + final File srcFile = fs.file( + fs.path.join(projectPath, 'lib', 'main.dart')); + final String nonFormatted = srcFile.readAsStringSync().replaceFirst( + 'main()', 'main( )'); + srcFile.writeAsStringSync(nonFormatted); + + final FormatCommand command = FormatCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + expect(runner.run(<String>[ + 'format', '--dry-run', '--set-exit-if-changed', srcFile.path, + ]), throwsException); + + final String shouldNotFormatted = srcFile.readAsStringSync(); + expect(shouldNotFormatted, nonFormatted); + }); + }); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/ide_config_test.dart b/packages/flutter_tools/test/general.shard/commands/ide_config_test.dart new file mode 100644 index 0000000..dd6defa --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/ide_config_test.dart
@@ -0,0 +1,324 @@ +// 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. + +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/template.dart'; +import 'package:flutter_tools/src/commands/ide_config.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('ide_config', () { + Directory tempDir; + Directory templateDir; + Directory intellijDir; + Directory toolsDir; + + Map<String, String> _getFilesystemContents([ Directory root ]) { + final String tempPath = tempDir.absolute.path; + final List<String> paths = + (root ?? tempDir).listSync(recursive: true).map((FileSystemEntity entity) { + final String relativePath = fs.path.relative(entity.path, from: tempPath); + return relativePath; + }).toList(); + final Map<String, String> contents = <String, String>{}; + for (String path in paths) { + final String absPath = fs.path.join(tempPath, path); + if (fs.isDirectorySync(absPath)) { + contents[path] = 'dir'; + } else if (fs.isFileSync(absPath)) { + contents[path] = fs.file(absPath).readAsStringSync(); + } + } + return contents; + } + + Map<String, String> _getManifest(Directory base, String marker, { bool isTemplate = false }) { + final String basePath = fs.path.relative(base.path, from: tempDir.absolute.path); + final String suffix = isTemplate ? Template.copyTemplateExtension : ''; + return <String, String>{ + fs.path.join(basePath, '.idea'): 'dir', + fs.path.join(basePath, '.idea', 'modules.xml$suffix'): 'modules $marker', + fs.path.join(basePath, '.idea', 'vcs.xml$suffix'): 'vcs $marker', + fs.path.join(basePath, '.idea', '.name$suffix'): + 'codeStyleSettings $marker', + fs.path.join(basePath, '.idea', 'runConfigurations'): 'dir', + fs.path.join(basePath, '.idea', 'runConfigurations', 'hello_world.xml$suffix'): + 'hello_world $marker', + fs.path.join(basePath, 'flutter.iml$suffix'): 'flutter $marker', + fs.path.join(basePath, 'packages', 'new', 'deep.iml$suffix'): 'deep $marker', + }; + } + + void _populateDir(Map<String, String> manifest) { + for (String key in manifest.keys) { + if (manifest[key] == 'dir') { + tempDir.childDirectory(key)..createSync(recursive: true); + } + } + for (String key in manifest.keys) { + if (manifest[key] != 'dir') { + tempDir.childFile(key) + ..createSync(recursive: true) + ..writeAsStringSync(manifest[key]); + } + } + } + + bool _fileOrDirectoryExists(String path) { + final String absPath = fs.path.join(tempDir.absolute.path, path); + return fs.file(absPath).existsSync() || fs.directory(absPath).existsSync(); + } + + Future<void> _updateIdeConfig({ + Directory dir, + List<String> args = const <String>[], + Map<String, String> expectedContents = const <String, String>{}, + List<String> unexpectedPaths = const <String>[], + }) async { + dir ??= tempDir; + final IdeConfigCommand command = IdeConfigCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + await runner.run(<String>[ + '--flutter-root=${tempDir.absolute.path}', + 'ide-config', + ...args, + ]); + + for (String path in expectedContents.keys) { + final String absPath = fs.path.join(tempDir.absolute.path, path); + expect(_fileOrDirectoryExists(fs.path.join(dir.path, path)), true, + reason: "$path doesn't exist"); + if (fs.file(absPath).existsSync()) { + expect(fs.file(absPath).readAsStringSync(), equals(expectedContents[path]), + reason: "$path contents don't match"); + } + } + for (String path in unexpectedPaths) { + expect(_fileOrDirectoryExists(fs.path.join(dir.path, path)), false, reason: '$path exists'); + } + } + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_ide_config_test.'); + final Directory packagesDir = tempDir.childDirectory('packages')..createSync(recursive: true); + toolsDir = packagesDir.childDirectory('flutter_tools')..createSync(); + templateDir = toolsDir.childDirectory('ide_templates')..createSync(); + intellijDir = templateDir.childDirectory('intellij')..createSync(); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + testUsingContext("doesn't touch existing files without --overwrite", () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'existing', + ); + _populateDir(templateManifest); + _populateDir(flutterManifest); + final Map<String, String> expectedContents = _getFilesystemContents(); + return _updateIdeConfig( + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + testUsingContext('creates non-existent files', () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'template', + ); + _populateDir(templateManifest); + final Map<String, String> expectedContents = <String, String>{ + ...templateManifest, + ...flutterManifest, + }; + return _updateIdeConfig( + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + testUsingContext('overwrites existing files with --overwrite', () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'existing', + ); + _populateDir(templateManifest); + _populateDir(flutterManifest); + final Map<String, String> overwrittenManifest = _getManifest( + tempDir, + 'template', + ); + final Map<String, String> expectedContents = <String, String>{ + ...templateManifest, + ...overwrittenManifest, + }; + return _updateIdeConfig( + args: <String>['--overwrite'], + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + testUsingContext('only adds new templates without --overwrite', () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + final String flutterIml = fs.path.join( + 'packages', + 'flutter_tools', + 'ide_templates', + 'intellij', + 'flutter.iml${Template.copyTemplateExtension}', + ); + templateManifest.remove(flutterIml); + _populateDir(templateManifest); + templateManifest[flutterIml] = 'flutter existing'; + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'existing', + ); + _populateDir(flutterManifest); + final Map<String, String> expectedContents = <String, String>{ + ...flutterManifest, + ...templateManifest, + }; + return _updateIdeConfig( + args: <String>['--update-templates'], + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + testUsingContext('update all templates with --overwrite', () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + _populateDir(templateManifest); + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'existing', + ); + _populateDir(flutterManifest); + final Map<String, String> updatedTemplates = _getManifest( + intellijDir, + 'existing', + isTemplate: true, + ); + final Map<String, String> expectedContents = <String, String>{ + ...flutterManifest, + ...updatedTemplates, + }; + return _updateIdeConfig( + args: <String>['--update-templates', '--overwrite'], + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + testUsingContext('removes deleted imls with --overwrite', () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + _populateDir(templateManifest); + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'existing', + ); + flutterManifest.remove('flutter.iml'); + _populateDir(flutterManifest); + final Map<String, String> updatedTemplates = _getManifest( + intellijDir, + 'existing', + isTemplate: true, + ); + final String flutterIml = fs.path.join( + 'packages', + 'flutter_tools', + 'ide_templates', + 'intellij', + 'flutter.iml${Template.copyTemplateExtension}', + ); + updatedTemplates.remove(flutterIml); + final Map<String, String> expectedContents = <String, String>{ + ...flutterManifest, + ...updatedTemplates, + }; + return _updateIdeConfig( + args: <String>['--update-templates', '--overwrite'], + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + testUsingContext('removes deleted imls with --overwrite, including empty parent dirs', () async { + final Map<String, String> templateManifest = _getManifest( + intellijDir, + 'template', + isTemplate: true, + ); + _populateDir(templateManifest); + final Map<String, String> flutterManifest = _getManifest( + tempDir, + 'existing', + ); + flutterManifest.remove(fs.path.join('packages', 'new', 'deep.iml')); + _populateDir(flutterManifest); + final Map<String, String> updatedTemplates = _getManifest( + intellijDir, + 'existing', + isTemplate: true, + ); + String deepIml = fs.path.join( + 'packages', + 'flutter_tools', + 'ide_templates', + 'intellij'); + // Remove the all the dir entries too. + updatedTemplates.remove(deepIml); + deepIml = fs.path.join(deepIml, 'packages'); + updatedTemplates.remove(deepIml); + deepIml = fs.path.join(deepIml, 'new'); + updatedTemplates.remove(deepIml); + deepIml = fs.path.join(deepIml, 'deep.iml'); + updatedTemplates.remove(deepIml); + final Map<String, String> expectedContents = <String, String>{ + ...flutterManifest, + ...updatedTemplates, + }; + return _updateIdeConfig( + args: <String>['--update-templates', '--overwrite'], + expectedContents: expectedContents, + ); + }, timeout: const Timeout.factor(2.0)); + + }); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/install_test.dart b/packages/flutter_tools/test/general.shard/commands/install_test.dart new file mode 100644 index 0000000..2883259 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/install_test.dart
@@ -0,0 +1,49 @@ +// Copyright 2015 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. + +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/install.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('install', () { + setUpAll(() { + Cache.disableLocking(); + }); + + testUsingContext('returns 0 when Android is connected and ready for an install', () async { + final InstallCommand command = InstallCommand(); + applyMocksToCommand(command); + + final MockAndroidDevice device = MockAndroidDevice(); + when(device.isAppInstalled(any)).thenAnswer((_) async => false); + when(device.installApp(any)).thenAnswer((_) async => true); + testDeviceManager.addDevice(device); + + await createTestCommandRunner(command).run(<String>['install']); + }, overrides: <Type, Generator>{ + Cache: () => MockCache(), + }); + + testUsingContext('returns 0 when iOS is connected and ready for an install', () async { + final InstallCommand command = InstallCommand(); + applyMocksToCommand(command); + + final MockIOSDevice device = MockIOSDevice(); + when(device.isAppInstalled(any)).thenAnswer((_) async => false); + when(device.installApp(any)).thenAnswer((_) async => true); + testDeviceManager.addDevice(device); + + await createTestCommandRunner(command).run(<String>['install']); + }, overrides: <Type, Generator>{ + Cache: () => MockCache(), + }); + }); +} + +class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/commands/packages_test.dart b/packages/flutter_tools/test/general.shard/commands/packages_test.dart new file mode 100644 index 0000000..8e0269b --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/packages_test.dart
@@ -0,0 +1,448 @@ +// Copyright 2016 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. + +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/base/file_system.dart' hide IOSink; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/utils.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/packages.dart'; +import 'package:flutter_tools/src/usage.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart' show MockProcessManager, MockStdio, PromptingProcess; + +class AlwaysTrueBotDetector implements BotDetector { + const AlwaysTrueBotDetector(); + + @override + bool get isRunningOnBot => true; +} + + +class AlwaysFalseBotDetector implements BotDetector { + const AlwaysFalseBotDetector(); + + @override + bool get isRunningOnBot => false; +} + + +void main() { + Cache.disableLocking(); + group('packages get/upgrade', () { + Directory tempDir; + + setUp(() { + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + Future<String> createProjectWithPlugin(String plugin, { List<String> arguments }) async { + final String projectPath = await createProject(tempDir, arguments: arguments); + final File pubspec = fs.file(fs.path.join(projectPath, 'pubspec.yaml')); + String content = await pubspec.readAsString(); + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n $plugin:\n', + ); + await pubspec.writeAsString(content, flush: true); + return projectPath; + } + + Future<PackagesCommand> runCommandIn(String projectPath, String verb, { List<String> args }) async { + final PackagesCommand command = PackagesCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + await runner.run(<String>[ + 'packages', + verb, + ...?args, + projectPath, + ]); + return command; + } + + void expectExists(String projectPath, String relPath) { + expect( + fs.isFileSync(fs.path.join(projectPath, relPath)), + true, + reason: '$projectPath/$relPath should exist, but does not', + ); + } + + void expectContains(String projectPath, String relPath, String substring) { + expectExists(projectPath, relPath); + expect( + fs.file(fs.path.join(projectPath, relPath)).readAsStringSync(), + contains(substring), + reason: '$projectPath/$relPath has unexpected content', + ); + } + + void expectNotExists(String projectPath, String relPath) { + expect( + fs.isFileSync(fs.path.join(projectPath, relPath)), + false, + reason: '$projectPath/$relPath should not exist, but does', + ); + } + + void expectNotContains(String projectPath, String relPath, String substring) { + expectExists(projectPath, relPath); + expect( + fs.file(fs.path.join(projectPath, relPath)).readAsStringSync(), + isNot(contains(substring)), + reason: '$projectPath/$relPath has unexpected content', + ); + } + + const List<String> pubOutput = <String>[ + '.packages', + 'pubspec.lock', + ]; + + const List<String> pluginRegistrants = <String>[ + 'ios/Runner/GeneratedPluginRegistrant.h', + 'ios/Runner/GeneratedPluginRegistrant.m', + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + ]; + + const List<String> modulePluginRegistrants = <String>[ + '.ios/Flutter/FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.h', + '.ios/Flutter/FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.m', + '.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + ]; + + const List<String> pluginWitnesses = <String>[ + '.flutter-plugins', + 'ios/Podfile', + ]; + + const List<String> modulePluginWitnesses = <String>[ + '.flutter-plugins', + '.ios/Podfile', + ]; + + const Map<String, String> pluginContentWitnesses = <String, String>{ + 'ios/Flutter/Debug.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"', + 'ios/Flutter/Release.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"', + }; + + const Map<String, String> modulePluginContentWitnesses = <String, String>{ + '.ios/Config/Debug.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"', + '.ios/Config/Release.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"', + }; + + void expectDependenciesResolved(String projectPath) { + for (String output in pubOutput) { + expectExists(projectPath, output); + } + } + + void expectZeroPluginsInjected(String projectPath) { + for (final String registrant in modulePluginRegistrants) { + expectExists(projectPath, registrant); + } + for (final String witness in pluginWitnesses) { + expectNotExists(projectPath, witness); + } + modulePluginContentWitnesses.forEach((String witness, String content) { + expectNotContains(projectPath, witness, content); + }); + } + + void expectPluginInjected(String projectPath) { + for (final String registrant in pluginRegistrants) { + expectExists(projectPath, registrant); + } + for (final String witness in pluginWitnesses) { + expectExists(projectPath, witness); + } + pluginContentWitnesses.forEach((String witness, String content) { + expectContains(projectPath, witness, content); + }); + } + + void expectModulePluginInjected(String projectPath) { + for (final String registrant in modulePluginRegistrants) { + expectExists(projectPath, registrant); + } + for (final String witness in modulePluginWitnesses) { + expectExists(projectPath, witness); + } + modulePluginContentWitnesses.forEach((String witness, String content) { + expectContains(projectPath, witness, content); + }); + } + + void removeGeneratedFiles(String projectPath) { + final Iterable<String> allFiles = <List<String>>[ + pubOutput, + modulePluginRegistrants, + pluginWitnesses, + ].expand<String>((List<String> list) => list); + for (String path in allFiles) { + final File file = fs.file(fs.path.join(projectPath, path)); + if (file.existsSync()) + file.deleteSync(); + } + } + + testUsingContext('get fetches packages', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'get'); + + expectDependenciesResolved(projectPath); + expectZeroPluginsInjected(projectPath); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('get --offline fetches packages', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'get', args: <String>['--offline']); + + expectDependenciesResolved(projectPath); + expectZeroPluginsInjected(projectPath); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('set the number of plugins as usage value', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + removeGeneratedFiles(projectPath); + + final PackagesCommand command = await runCommandIn(projectPath, 'get'); + final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand; + + expect(await getCommand.usageValues, containsPair(kCommandPackagesNumberPlugins, '0')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('indicate that the project is not a module in usage value', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub']); + removeGeneratedFiles(projectPath); + + final PackagesCommand command = await runCommandIn(projectPath, 'get'); + final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand; + + expect(await getCommand.usageValues, containsPair(kCommandPackagesProjectModule, 'false')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('indicate that the project is a module in usage value', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + removeGeneratedFiles(projectPath); + + final PackagesCommand command = await runCommandIn(projectPath, 'get'); + final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand; + + expect(await getCommand.usageValues, containsPair(kCommandPackagesProjectModule, 'true')); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('upgrade fetches packages', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'upgrade'); + + expectDependenciesResolved(projectPath); + expectZeroPluginsInjected(projectPath); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('get fetches packages and injects plugin', () async { + final String projectPath = await createProjectWithPlugin('path_provider', + arguments: <String>['--no-pub', '--template=module']); + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'get'); + + expectDependenciesResolved(projectPath); + expectModulePluginInjected(projectPath); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('get fetches packages and injects plugin in plugin project', () async { + final String projectPath = await createProject( + tempDir, + arguments: <String>['--template=plugin', '--no-pub'], + ); + final String exampleProjectPath = fs.path.join(projectPath, 'example'); + removeGeneratedFiles(projectPath); + removeGeneratedFiles(exampleProjectPath); + + await runCommandIn(projectPath, 'get'); + + expectDependenciesResolved(projectPath); + + await runCommandIn(exampleProjectPath, 'get'); + + expectDependenciesResolved(exampleProjectPath); + expectPluginInjected(exampleProjectPath); + }, timeout: allowForRemotePubInvocation); + }); + + group('packages test/pub', () { + MockProcessManager mockProcessManager; + MockStdio mockStdio; + + setUp(() { + mockProcessManager = MockProcessManager(); + mockStdio = MockStdio(); + }); + + testUsingContext('test without bot', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'test']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(3)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], 'run'); + expect(commands[2], 'test'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysFalseBotDetector(), + }); + + testUsingContext('test with bot', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'test']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(4)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'run'); + expect(commands[3], 'test'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + + testUsingContext('run', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', '--verbose', 'pub', 'run', '--foo', 'bar']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(4)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], 'run'); + expect(commands[2], '--foo'); + expect(commands[3], 'bar'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + }); + + testUsingContext('pub publish', () async { + final PromptingProcess process = PromptingProcess(); + mockProcessManager.processFactory = (List<String> commands) => process; + final Future<void> runPackages = createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'pub', 'publish']); + final Future<void> runPrompt = process.showPrompt('Proceed (y/n)? ', <String>['hello', 'world']); + final Future<void> simulateUserInput = Future<void>(() { + mockStdio.simulateStdin('y'); + }); + await Future.wait<void>(<Future<void>>[runPackages, runPrompt, simulateUserInput]); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(2)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], 'publish'); + final List<String> stdout = mockStdio.writtenToStdout; + expect(stdout, hasLength(4)); + expect(stdout.sublist(0, 2), contains('Proceed (y/n)? ')); + expect(stdout.sublist(0, 2), contains('y\n')); + expect(stdout[2], 'hello\n'); + expect(stdout[3], 'world\n'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + }); + + testUsingContext('publish', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'publish']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(3)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'publish'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + + testUsingContext('deps', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'deps']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(3)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'deps'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + + testUsingContext('cache', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'cache']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(3)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'cache'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + + testUsingContext('version', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'version']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(3)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'version'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + + testUsingContext('uploader', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'uploader']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(3)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'uploader'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + + testUsingContext('global', () async { + await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'global', 'list']); + final List<String> commands = mockProcessManager.commands; + expect(commands, hasLength(4)); + expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub')); + expect(commands[1], '--trace'); + expect(commands[2], 'global'); + expect(commands[3], 'list'); + }, overrides: <Type, Generator>{ + ProcessManager: () => mockProcessManager, + Stdio: () => mockStdio, + BotDetector: () => const AlwaysTrueBotDetector(), + }); + }); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/precache_test.dart b/packages/flutter_tools/test/general.shard/commands/precache_test.dart new file mode 100644 index 0000000..0308da6 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/precache_test.dart
@@ -0,0 +1,87 @@ +// Copyright 2019 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. + +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/precache.dart'; +import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:flutter_tools/src/version.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('precache', () { + final MockCache cache = MockCache(); + Set<DevelopmentArtifact> artifacts; + + when(cache.isUpToDate()).thenReturn(false); + when(cache.updateAll(any)).thenAnswer((Invocation invocation) { + artifacts = invocation.positionalArguments.first; + return Future<void>.value(null); + }); + + testUsingContext('Adds artifact flags to requested artifacts', () async { + final PrecacheCommand command = PrecacheCommand(); + applyMocksToCommand(command); + await createTestCommandRunner(command).run( + const <String>['precache', '--ios', '--android', '--web', '--macos', '--linux', '--windows', '--fuchsia'] + ); + expect(artifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.iOS, + DevelopmentArtifact.android, + DevelopmentArtifact.web, + DevelopmentArtifact.macOS, + DevelopmentArtifact.linux, + DevelopmentArtifact.windows, + DevelopmentArtifact.fuchsia, + })); + }, overrides: <Type, Generator>{ + Cache: () => cache, + }); + + final MockFlutterVersion flutterVersion = MockFlutterVersion(); + when(flutterVersion.isMaster).thenReturn(false); + + testUsingContext('Adds artifact flags to requested artifacts on stable', () async { + // Release lock between test cases. + Cache.releaseLockEarly(); + final PrecacheCommand command = PrecacheCommand(); + applyMocksToCommand(command); + await createTestCommandRunner(command).run( + const <String>['precache', '--ios', '--android', '--web', '--macos', '--linux', '--windows', '--fuchsia'] + ); + expect(artifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.iOS, + DevelopmentArtifact.android, + })); + }, overrides: <Type, Generator>{ + Cache: () => cache, + FlutterVersion: () => flutterVersion, + }); + + testUsingContext('Downloads artifacts when --force is provided', () async { + when(cache.isUpToDate()).thenReturn(true); + // Release lock between test cases. + Cache.releaseLockEarly(); + final PrecacheCommand command = PrecacheCommand(); + applyMocksToCommand(command); + await createTestCommandRunner(command).run(const <String>['precache', '--force']); + expect(artifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.iOS, + DevelopmentArtifact.android, + })); + }, overrides: <Type, Generator>{ + Cache: () => cache, + FlutterVersion: () => flutterVersion, + }); + }); +} + +class MockFlutterVersion extends Mock implements FlutterVersion {} +class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/commands/run_test.dart b/packages/flutter_tools/test/general.shard/commands/run_test.dart new file mode 100644 index 0000000..b743a36 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/run_test.dart
@@ -0,0 +1,262 @@ +// Copyright 2016 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. + +import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/application_package.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/run.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:flutter_tools/src/version.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('run', () { + MockApplicationPackageFactory mockApplicationPackageFactory; + MockDeviceManager mockDeviceManager; + MockFlutterVersion mockStableFlutterVersion; + MockFlutterVersion mockUnstableFlutterVersion; + + setUpAll(() { + Cache.disableLocking(); + mockApplicationPackageFactory = MockApplicationPackageFactory(); + mockDeviceManager = MockDeviceManager(); + mockStableFlutterVersion = MockFlutterVersion(isStable: true); + mockUnstableFlutterVersion = MockFlutterVersion(isStable: false); + }); + + testUsingContext('fails when target not found', () async { + final RunCommand command = RunCommand(); + applyMocksToCommand(command); + try { + await createTestCommandRunner(command).run(<String>['run', '-t', 'abc123']); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode ?? 1, 1); + } + }); + + + group('dart-flags option', () { + setUpAll(() { + when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream<Device>.fromIterable(<Device>[ + FakeDevice(), + ]); + }); + }); + + RunCommand command; + List<String> args; + setUp(() { + command = TestRunCommand(); + args = <String> [ + 'run', + '--dart-flags', '"--observe"', + '--no-hot', + ]; + }); + + testUsingContext('is not available on stable channel', () async { + // Stable branch. + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + // ignore: unused_catch_clause + } on UsageException catch(e) { + // Not available while on stable branch. + } + }, overrides: <Type, Generator>{ + DeviceManager: () => mockDeviceManager, + FlutterVersion: () => mockStableFlutterVersion, + }); + + testUsingContext('is populated in debug mode', () async { + // FakeDevice.startApp checks that --dart-flags doesn't get dropped and + // throws ToolExit with FakeDevice.kSuccess if the flag is populated. + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode, FakeDevice.kSuccess); + } + }, overrides: <Type, Generator>{ + ApplicationPackageFactory: () => mockApplicationPackageFactory, + DeviceManager: () => mockDeviceManager, + FlutterVersion: () => mockUnstableFlutterVersion, + }); + + testUsingContext('is populated in profile mode', () async { + args.add('--profile'); + + // FakeDevice.startApp checks that --dart-flags doesn't get dropped and + // throws ToolExit with FakeDevice.kSuccess if the flag is populated. + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode, FakeDevice.kSuccess); + } + }, overrides: <Type, Generator>{ + ApplicationPackageFactory: () => mockApplicationPackageFactory, + DeviceManager: () => mockDeviceManager, + FlutterVersion: () => mockUnstableFlutterVersion, + }); + + testUsingContext('is not populated in release mode', () async { + args.add('--release'); + + // FakeDevice.startApp checks that --dart-flags *does* get dropped and + // throws ToolExit with FakeDevice.kSuccess if the flag is set to the + // empty string. + try { + await createTestCommandRunner(command).run(args); + fail('Expect exception'); + } on ToolExit catch (e) { + expect(e.exitCode, FakeDevice.kSuccess); + } + }, overrides: <Type, Generator>{ + ApplicationPackageFactory: () => mockApplicationPackageFactory, + DeviceManager: () => mockDeviceManager, + FlutterVersion: () => mockUnstableFlutterVersion, + }); + }); + + testUsingContext('should only request artifacts corresponding to connected devices', () async { + when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream<Device>.fromIterable(<Device>[ + MockDevice(TargetPlatform.android_arm), + ]); + }); + + expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.android, + })); + + when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream<Device>.fromIterable(<Device>[ + MockDevice(TargetPlatform.ios), + ]); + }); + + expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.iOS, + })); + + when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream<Device>.fromIterable(<Device>[ + MockDevice(TargetPlatform.ios), + MockDevice(TargetPlatform.android_arm), + ]); + }); + + expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.iOS, + DevelopmentArtifact.android, + })); + + when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream<Device>.fromIterable(<Device>[ + MockDevice(TargetPlatform.web_javascript), + ]); + }); + + expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ + DevelopmentArtifact.universal, + DevelopmentArtifact.web, + })); + }, overrides: <Type, Generator>{ + DeviceManager: () => mockDeviceManager, + }); + }); +} + +class MockDeviceManager extends Mock implements DeviceManager {} +class MockDevice extends Mock implements Device { + MockDevice(this._targetPlatform); + + final TargetPlatform _targetPlatform; + + @override + Future<TargetPlatform> get targetPlatform async => _targetPlatform; +} + +class TestRunCommand extends RunCommand { + @override + // ignore: must_call_super + Future<void> validateCommand() async { + devices = await deviceManager.getDevices().toList(); + } +} + +class MockStableFlutterVersion extends MockFlutterVersion { + @override + bool get isMaster => false; +} + +class FakeDevice extends Fake implements Device { + static const int kSuccess = 1; + static const int kFailure = -1; + final TargetPlatform _targetPlatform = TargetPlatform.ios; + + void _throwToolExit(int code) => throwToolExit(null, exitCode: code); + + @override + Future<bool> get isLocalEmulator => Future<bool>.value(false); + + @override + bool get supportsHotReload => false; + + @override + Future<String> get sdkNameAndVersion => Future<String>.value(''); + + @override + DeviceLogReader getLogReader({ ApplicationPackage app }) { + return MockDeviceLogReader(); + } + + @override + String get name => 'FakeDevice'; + + @override + Future<TargetPlatform> get targetPlatform async => _targetPlatform; + + @override + Future<LaunchResult> startApp( + ApplicationPackage package, { + String mainPath, + String route, + DebuggingOptions debuggingOptions, + Map<String, dynamic> platformArgs, + bool prebuiltApplication = false, + bool usesTerminalUi = true, + bool ipv6 = false, + }) async { + final String dartFlags = debuggingOptions.dartFlags; + // In release mode, --dart-flags should be set to the empty string and + // provided flags should be dropped. In debug and profile modes, + // --dart-flags should not be empty. + if (debuggingOptions.buildInfo.isRelease) { + if (dartFlags.isNotEmpty) { + _throwToolExit(kFailure); + } + _throwToolExit(kSuccess); + } else { + if (dartFlags.isEmpty) { + _throwToolExit(kFailure); + } + _throwToolExit(kSuccess); + } + return null; + } +}
diff --git a/packages/flutter_tools/test/general.shard/commands/shell_completion_test.dart b/packages/flutter_tools/test/general.shard/commands/shell_completion_test.dart new file mode 100644 index 0000000..00b4aac --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/shell_completion_test.dart
@@ -0,0 +1,91 @@ +// Copyright 2018 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. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/shell_completion.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('shell_completion', () { + MockStdio mockStdio; + + setUp(() { + Cache.disableLocking(); + mockStdio = MockStdio(); + }); + + testUsingContext('generates bash initialization script to stdout', () async { + final ShellCompletionCommand command = ShellCompletionCommand(); + await createTestCommandRunner(command).run(<String>['bash-completion']); + expect(mockStdio.writtenToStdout.length, equals(1)); + expect(mockStdio.writtenToStdout.first, contains('__flutter_completion')); + }, overrides: <Type, Generator>{ + Stdio: () => mockStdio, + }); + + testUsingContext('generates bash initialization script to stdout with arg', () async { + final ShellCompletionCommand command = ShellCompletionCommand(); + await createTestCommandRunner(command).run(<String>['bash-completion', '-']); + expect(mockStdio.writtenToStdout.length, equals(1)); + expect(mockStdio.writtenToStdout.first, contains('__flutter_completion')); + }, overrides: <Type, Generator>{ + Stdio: () => mockStdio, + }); + + testUsingContext('generates bash initialization script to output file', () async { + final ShellCompletionCommand command = ShellCompletionCommand(); + const String outputFile = 'bash-setup.sh'; + await createTestCommandRunner(command).run( + <String>['bash-completion', outputFile], + ); + expect(fs.isFileSync(outputFile), isTrue); + expect(fs.file(outputFile).readAsStringSync(), contains('__flutter_completion')); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + Stdio: () => mockStdio, + }); + + testUsingContext("won't overwrite existing output file ", () async { + final ShellCompletionCommand command = ShellCompletionCommand(); + const String outputFile = 'bash-setup.sh'; + fs.file(outputFile).createSync(); + try { + await createTestCommandRunner(command).run( + <String>['bash-completion', outputFile], + ); + fail('Expect ToolExit exception'); + } on ToolExit catch (error) { + expect(error.exitCode ?? 1, 1); + expect(error.message, contains('Use --overwrite')); + } + expect(fs.isFileSync(outputFile), isTrue); + expect(fs.file(outputFile).readAsStringSync(), isEmpty); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + Stdio: () => mockStdio, + }); + + testUsingContext('will overwrite existing output file if given --overwrite', () async { + final ShellCompletionCommand command = ShellCompletionCommand(); + const String outputFile = 'bash-setup.sh'; + fs.file(outputFile).createSync(); + await createTestCommandRunner(command).run( + <String>['bash-completion', '--overwrite', outputFile], + ); + expect(fs.isFileSync(outputFile), isTrue); + expect(fs.file(outputFile).readAsStringSync(), contains('__flutter_completion')); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + Stdio: () => mockStdio, + }); + }); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/test_test.dart b/packages/flutter_tools/test/general.shard/commands/test_test.dart new file mode 100644 index 0000000..348ce99 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/test_test.dart
@@ -0,0 +1,221 @@ +// Copyright 2015 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. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/dart/sdk.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +// This test depends on some files in ///dev/automated_tests/flutter_test/* + +Future<void> _testExclusionLock; + +void main() { + group('flutter test should', () { + + final String automatedTestsDirectory = fs.path.join('..', '..', 'dev', 'automated_tests'); + final String flutterTestDirectory = fs.path.join(automatedTestsDirectory, 'flutter_test'); + + testUsingContext('not have extraneous error messages', () async { + Cache.flutterRoot = '../..'; + return _testFile('trivial_widget', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero); + }, skip: io.Platform.isLinux); // Flutter on Linux sometimes has problems with font resolution (#7224) + + testUsingContext('report nice errors for exceptions thrown within testWidgets()', () async { + Cache.flutterRoot = '../..'; + return _testFile('exception_handling', automatedTestsDirectory, flutterTestDirectory); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report a nice error when a guarded function was called without await', () async { + Cache.flutterRoot = '../..'; + return _testFile('test_async_utils_guarded', automatedTestsDirectory, flutterTestDirectory); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report a nice error when an async function was called without await', () async { + Cache.flutterRoot = '../..'; + return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report a nice error when a Ticker is left running', () async { + Cache.flutterRoot = '../..'; + return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async { + final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests'); + Cache.flutterRoot = '../..'; + return _testFile('trivial', missingDependencyTests, missingDependencyTests); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report which user created widget caused the error', () async { + Cache.flutterRoot = '../..'; + return _testFile('print_user_created_ancestor', automatedTestsDirectory, flutterTestDirectory, + extraArguments: const <String>['--track-widget-creation']); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report which user created widget caused the error - no flag', () async { + Cache.flutterRoot = '../..'; + return _testFile('print_user_created_ancestor_no_flag', automatedTestsDirectory, flutterTestDirectory); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('run a test when its name matches a regexp', () async { + Cache.flutterRoot = '../..'; + final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory, + extraArguments: const <String>['--name', 'inc.*de']); + if (!result.stdout.contains('+1: All tests passed')) + fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n'); + expect(result.exitCode, 0); + }); + + testUsingContext('run a test when its name contains a string', () async { + Cache.flutterRoot = '../..'; + final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory, + extraArguments: const <String>['--plain-name', 'include']); + if (!result.stdout.contains('+1: All tests passed')) + fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n'); + expect(result.exitCode, 0); + }); + + testUsingContext('test runs to completion', () async { + Cache.flutterRoot = '../..'; + final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory, + extraArguments: const <String>['--verbose']); + if ((!result.stdout.contains('+1: All tests passed')) || + (!result.stdout.contains('test 0: starting shell process')) || + (!result.stdout.contains('test 0: deleting temporary directory')) || + (!result.stdout.contains('test 0: finished')) || + (!result.stdout.contains('test package returned with exit code 0'))) + fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n'); + if (result.stderr.isNotEmpty) + fail('unexpected error output from test:\n\n${result.stderr}\n-- end stderr --\n\n'); + expect(result.exitCode, 0); + }); + + }); +} + +Future<void> _testFile( + String testName, + String workingDirectory, + String testDirectory, { + Matcher exitCode, + List<String> extraArguments = const <String>[], + }) async { + exitCode ??= isNonZero; + final String fullTestExpectation = fs.path.join(testDirectory, '${testName}_expectation.txt'); + final File expectationFile = fs.file(fullTestExpectation); + if (!expectationFile.existsSync()) + fail('missing expectation file: $expectationFile'); + + while (_testExclusionLock != null) + await _testExclusionLock; + + final ProcessResult exec = await _runFlutterTest( + testName, + workingDirectory, + testDirectory, + extraArguments: extraArguments, + ); + + expect(exec.exitCode, exitCode); + final List<String> output = exec.stdout.split('\n'); + if (output.first == 'Waiting for another flutter command to release the startup lock...') + output.removeAt(0); + if (output.first.startsWith('Running "flutter pub get" in')) + output.removeAt(0); + output.add('<<stderr>>'); + output.addAll(exec.stderr.split('\n')); + final List<String> expectations = fs.file(fullTestExpectation).readAsLinesSync(); + bool allowSkip = false; + int expectationLineNumber = 0; + int outputLineNumber = 0; + bool haveSeenStdErrMarker = false; + while (expectationLineNumber < expectations.length) { + expect( + output, + hasLength(greaterThan(outputLineNumber)), + reason: 'Failure in $testName to compare to $fullTestExpectation', + ); + final String expectationLine = expectations[expectationLineNumber]; + String outputLine = output[outputLineNumber]; + if (expectationLine == '<<skip until matching line>>') { + allowSkip = true; + expectationLineNumber += 1; + continue; + } + if (allowSkip) { + if (!RegExp(expectationLine).hasMatch(outputLine)) { + outputLineNumber += 1; + continue; + } + allowSkip = false; + } + if (expectationLine == '<<stderr>>') { + expect(haveSeenStdErrMarker, isFalse); + haveSeenStdErrMarker = true; + } + if (!RegExp(expectationLine).hasMatch(outputLine) && outputLineNumber + 1 < output.length) { + // Check if the RegExp can match the next two lines in the output so + // that it is possible to write expectations that still hold even if a + // line is wrapped slightly differently due to for example a file name + // being longer on one platform than another. + final String mergedLines = '$outputLine\n${output[outputLineNumber+1]}'; + if (RegExp(expectationLine).hasMatch(mergedLines)) { + outputLineNumber += 1; + outputLine = mergedLines; + } + } + + expect(outputLine, matches(expectationLine), reason: 'Full output:\n- - - -----8<----- - - -\n${output.join("\n")}\n- - - -----8<----- - - -'); + expectationLineNumber += 1; + outputLineNumber += 1; + } + expect(allowSkip, isFalse); + if (!haveSeenStdErrMarker) + expect(exec.stderr, ''); +} + +Future<ProcessResult> _runFlutterTest( + String testName, + String workingDirectory, + String testDirectory, { + List<String> extraArguments = const <String>[], +}) async { + + final String testFilePath = fs.path.join(testDirectory, '${testName}_test.dart'); + final File testFile = fs.file(testFilePath); + if (!testFile.existsSync()) + fail('missing test file: $testFile'); + + final List<String> args = <String>[ + ...dartVmFlags, + fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')), + 'test', + '--no-color', + ...extraArguments, + testFilePath + ]; + + while (_testExclusionLock != null) + await _testExclusionLock; + + final Completer<void> testExclusionCompleter = Completer<void>(); + _testExclusionLock = testExclusionCompleter.future; + try { + return await Process.run( + fs.path.join(dartSdkPath, 'bin', 'dart'), + args, + workingDirectory: workingDirectory, + ); + } finally { + _testExclusionLock = null; + testExclusionCompleter.complete(); + } +}
diff --git a/packages/flutter_tools/test/general.shard/commands/update_packages_test.dart b/packages/flutter_tools/test/general.shard/commands/update_packages_test.dart new file mode 100644 index 0000000..6097ba0 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/update_packages_test.dart
@@ -0,0 +1,18 @@ +// Copyright 2015 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. + +import 'package:flutter_tools/src/commands/update_packages.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('UpdatePackagesCommand', () { + // Marking it as experimental breaks bots tests and packaging scripts on stable branches. + testUsingContext('is not marked as experimental', () async { + final UpdatePackagesCommand command = UpdatePackagesCommand(); + expect(command.isExperimental, isFalse); + }); + }); +}
diff --git a/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart b/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart new file mode 100644 index 0000000..ac9879d --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart
@@ -0,0 +1,192 @@ +// Copyright 2016 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. + +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/upgrade.dart'; +import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:flutter_tools/src/version.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('UpgradeCommandRunner', () { + FakeUpgradeCommandRunner fakeCommandRunner; + UpgradeCommandRunner realCommandRunner; + MockProcessManager processManager; + final MockFlutterVersion flutterVersion = MockFlutterVersion(); + const GitTagVersion gitTagVersion = GitTagVersion(1, 2, 3, 4, 5, 'asd'); + when(flutterVersion.channel).thenReturn('dev'); + + setUp(() { + fakeCommandRunner = FakeUpgradeCommandRunner(); + realCommandRunner = UpgradeCommandRunner(); + processManager = MockProcessManager(); + fakeCommandRunner.willHaveUncomittedChanges = false; + }); + + test('throws on unknown tag, official branch, noforce', () async { + final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( + false, + const GitTagVersion.unknown(), + flutterVersion, + ); + expect(result, throwsA(isInstanceOf<ToolExit>())); + }); + + test('does not throw on unknown tag, official branch, force', () async { + final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( + true, + const GitTagVersion.unknown(), + flutterVersion, + ); + expect(await result, null); + }); + + test('throws tool exit with uncommitted changes', () async { + fakeCommandRunner.willHaveUncomittedChanges = true; + final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( + false, + gitTagVersion, + flutterVersion, + ); + expect(result, throwsA(isA<ToolExit>())); + }); + + test('does not throw tool exit with uncommitted changes and force', () async { + fakeCommandRunner.willHaveUncomittedChanges = true; + final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( + true, + gitTagVersion, + flutterVersion, + ); + expect(await result, null); + }); + + test('Doesn\'t throw on known tag, dev branch, no force', () async { + final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( + false, + gitTagVersion, + flutterVersion, + ); + expect(await result, null); + }); + + testUsingContext('verifyUpstreamConfigured', () async { + when(processManager.run( + <String>['git', 'rev-parse', '@{u}'], + environment:anyNamed('environment'), + workingDirectory: anyNamed('workingDirectory')) + ).thenAnswer((Invocation invocation) async { + return FakeProcessResult() + ..exitCode = 0; + }); + await realCommandRunner.verifyUpstreamConfigured(); + }, overrides: <Type, Generator>{ + ProcessManager: () => processManager, + }); + }); + + group('matchesGitLine', () { + setUpAll(() { + Cache.disableLocking(); + }); + + bool _match(String line) => UpgradeCommandRunner.matchesGitLine(line); + + test('regex match', () { + expect(_match(' .../flutter_gallery/lib/demo/buttons_demo.dart | 10 +--'), true); + expect(_match(' dev/benchmarks/complex_layout/lib/main.dart | 24 +-'), true); + + expect(_match(' rename {packages/flutter/doc => dev/docs}/styles.html (92%)'), true); + expect(_match(' delete mode 100644 doc/index.html'), true); + expect(_match(' create mode 100644 examples/flutter_gallery/lib/gallery/demo.dart'), true); + + expect(_match('Fast-forward'), true); + }); + + test('regex doesn\'t match', () { + expect(_match('Updating 79cfe1e..5046107'), false); + expect(_match('229 files changed, 6179 insertions(+), 3065 deletions(-)'), false); + }); + + group('findProjectRoot', () { + Directory tempDir; + + setUp(() async { + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_upgrade_test.'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + testUsingContext('in project', () async { + final String projectPath = await createProject(tempDir); + expect(findProjectRoot(projectPath), projectPath); + expect(findProjectRoot(fs.path.join(projectPath, 'lib')), projectPath); + + final String hello = fs.path.join(Cache.flutterRoot, 'examples', 'hello_world'); + expect(findProjectRoot(hello), hello); + expect(findProjectRoot(fs.path.join(hello, 'lib')), hello); + }); + + testUsingContext('outside project', () async { + final String projectPath = await createProject(tempDir); + expect(findProjectRoot(fs.directory(projectPath).parent.path), null); + expect(findProjectRoot(Cache.flutterRoot), null); + }); + }); + }); +} + +class FakeUpgradeCommandRunner extends UpgradeCommandRunner { + bool willHaveUncomittedChanges = false; + + @override + Future<void> verifyUpstreamConfigured() async {} + + @override + Future<bool> hasUncomittedChanges() async => willHaveUncomittedChanges; + + @override + Future<void> resetChanges(GitTagVersion gitTagVersion) async {} + + @override + Future<void> upgradeChannel(FlutterVersion flutterVersion) async {} + + @override + Future<void> attemptFastForward() async {} + + @override + Future<void> precacheArtifacts() async {} + + @override + Future<void> updatePackages(FlutterVersion flutterVersion) async {} + + @override + Future<void> runDoctor() async {} +} + +class MockFlutterVersion extends Mock implements FlutterVersion {} +class MockProcessManager extends Mock implements ProcessManager {} +class FakeProcessResult implements ProcessResult { + @override + int exitCode; + + @override + int pid = 0; + + @override + String stderr = ''; + + @override + String stdout = ''; +}
diff --git a/packages/flutter_tools/test/general.shard/commands/version_test.dart b/packages/flutter_tools/test/general.shard/commands/version_test.dart new file mode 100644 index 0000000..1e48004 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/version_test.dart
@@ -0,0 +1,123 @@ +// Copyright 2019 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/version.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart' show MockProcess; + +void main() { + group('version', () { + setUpAll(() { + Cache.disableLocking(); + }); + + testUsingContext('version ls', () async { + final VersionCommand command = VersionCommand(); + await createTestCommandRunner(command).run(<String>['version']); + expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n' '')); + }, overrides: <Type, Generator>{ + ProcessManager: () => MockProcessManager(), + }); + + testUsingContext('version switch', () async { + const String version = '10.0.0'; + final VersionCommand command = VersionCommand(); + final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', version]); + await Future.wait<void>(<Future<void>>[runCommand]); + expect(testLogger.statusText, contains('Switching Flutter to version $version')); + }, overrides: <Type, Generator>{ + ProcessManager: () => MockProcessManager(), + }); + + testUsingContext('switch to not supported version without force', () async { + const String version = '1.1.5'; + final VersionCommand command = VersionCommand(); + final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', version]); + await Future.wait<void>(<Future<void>>[runCommand]); + expect(testLogger.errorText, contains('Version command is not supported in')); + }, overrides: <Type, Generator>{ + ProcessManager: () => MockProcessManager(), + }); + + testUsingContext('switch to not supported version with force', () async { + const String version = '1.1.5'; + final VersionCommand command = VersionCommand(); + final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', '--force', version]); + await Future.wait<void>(<Future<void>>[runCommand]); + expect(testLogger.statusText, contains('Switching Flutter to version $version with force')); + }, overrides: <Type, Generator>{ + ProcessManager: () => MockProcessManager(), + }); + }); +} + +class MockProcessManager extends Mock implements ProcessManager { + String version = ''; + + @override + Future<ProcessResult> run( + List<dynamic> command, { + String workingDirectory, + Map<String, String> environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding stdoutEncoding = systemEncoding, + Encoding stderrEncoding = systemEncoding, + }) async { + if (command[0] == 'git' && command[1] == 'tag') { + return ProcessResult(0, 0, 'v10.0.0\r\nv20.0.0', ''); + } + if (command[0] == 'git' && command[1] == 'checkout') { + version = command[2]; + } + return ProcessResult(0, 0, '', ''); + } + + @override + ProcessResult runSync( + List<dynamic> command, { + String workingDirectory, + Map<String, String> environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding stdoutEncoding = systemEncoding, + Encoding stderrEncoding = systemEncoding, + }) { + final String commandStr = command.join(' '); + if (commandStr == 'git log -n 1 --pretty=format:%H') { + return ProcessResult(0, 0, '000000000000000000000', ''); + } + if (commandStr == + 'git describe --match v*.*.* --first-parent --long --tags') { + if (version.isNotEmpty) { + return ProcessResult(0, 0, '$version-0-g00000000', ''); + } + } + return ProcessResult(0, 0, '', ''); + } + + @override + Future<Process> start( + List<dynamic> command, { + String workingDirectory, + Map<String, String> environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal, + }) { + final Completer<Process> completer = Completer<Process>(); + completer.complete(MockProcess()); + return completer.future; + } +}