| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:file/memory.dart'; |
| import 'package:file_testing/file_testing.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/version.dart'; |
| import 'package:flutter_tools/src/commands/clean.dart'; |
| import 'package:flutter_tools/src/ios/xcodeproj.dart'; |
| import 'package:flutter_tools/src/macos/xcode.dart'; |
| import 'package:flutter_tools/src/project.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:test/fake.dart'; |
| |
| import '../../src/common.dart'; |
| import '../../src/context.dart'; |
| |
| void main() { |
| group('clean command', () { |
| late Xcode xcode; |
| late FakeXcodeProjectInterpreter xcodeProjectInterpreter; |
| |
| setUp(() { |
| xcodeProjectInterpreter = FakeXcodeProjectInterpreter(); |
| xcode = Xcode.test( |
| processManager: FakeProcessManager.any(), |
| xcodeProjectInterpreter: xcodeProjectInterpreter, |
| ); |
| }); |
| |
| group('general', () { |
| late MemoryFileSystem fs; |
| late Directory buildDirectory; |
| |
| setUp(() { |
| fs = MemoryFileSystem.test(); |
| |
| final Directory currentDirectory = fs.currentDirectory; |
| buildDirectory = currentDirectory.childDirectory('build'); |
| buildDirectory.createSync(recursive: true); |
| }); |
| |
| testUsingContext('$CleanCommand removes build and .dart_tool and ephemeral directories, cleans Xcode for iOS and macOS', () async { |
| final FlutterProject projectUnderTest = setupProjectUnderTest(fs.currentDirectory); |
| // Xcode is installed and version satisfactory. |
| xcodeProjectInterpreter.isInstalled = true; |
| xcodeProjectInterpreter.version = Version(1000, 0, 0); |
| await CleanCommand().runCommand(); |
| |
| expect(buildDirectory, isNot(exists)); |
| expect(projectUnderTest.dartTool, isNot(exists)); |
| expect(projectUnderTest.android.ephemeralDirectory, isNot(exists)); |
| |
| expect(projectUnderTest.ios.ephemeralDirectory, isNot(exists)); |
| expect(projectUnderTest.ios.ephemeralModuleDirectory, isNot(exists)); |
| expect(projectUnderTest.ios.generatedXcodePropertiesFile, isNot(exists)); |
| expect(projectUnderTest.ios.generatedEnvironmentVariableExportScript, isNot(exists)); |
| expect(projectUnderTest.ios.deprecatedCompiledDartFramework, isNot(exists)); |
| expect(projectUnderTest.ios.deprecatedProjectFlutterFramework, isNot(exists)); |
| expect(projectUnderTest.ios.flutterPodspec, isNot(exists)); |
| |
| expect(projectUnderTest.linux.ephemeralDirectory, isNot(exists)); |
| expect(projectUnderTest.macos.ephemeralDirectory, isNot(exists)); |
| expect(projectUnderTest.windows.ephemeralDirectory, isNot(exists)); |
| |
| expect(projectUnderTest.flutterPluginsFile, isNot(exists)); |
| expect(projectUnderTest.flutterPluginsDependenciesFile, isNot(exists)); |
| expect(projectUnderTest.packagesFile, isNot(exists)); |
| |
| expect(xcodeProjectInterpreter.workspaces, const <CleanWorkspaceCall>[ |
| CleanWorkspaceCall('/ios/Runner.xcworkspace', 'Runner', false), |
| CleanWorkspaceCall('/macos/Runner.xcworkspace', 'Runner', false), |
| ]); |
| }, overrides: <Type, Generator>{ |
| FileSystem: () => fs, |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, |
| }); |
| |
| testUsingContext('$CleanCommand cleans Xcode verbosely for iOS and macOS', () async { |
| setupProjectUnderTest(fs.currentDirectory); |
| // Xcode is installed and version satisfactory. |
| xcodeProjectInterpreter.isInstalled = true; |
| xcodeProjectInterpreter.version = Version(1000, 0, 0); |
| |
| await CleanCommand(verbose: true).runCommand(); |
| |
| expect(xcodeProjectInterpreter.workspaces, const <CleanWorkspaceCall>[ |
| CleanWorkspaceCall('/ios/Runner.xcworkspace', 'Runner', true), |
| CleanWorkspaceCall('/macos/Runner.xcworkspace', 'Runner', true), |
| ]); |
| }, overrides: <Type, Generator>{ |
| FileSystem: () => fs, |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, |
| }); |
| }); |
| |
| group('Windows', () { |
| late FakePlatform windowsPlatform; |
| late MemoryFileSystem fileSystem; |
| late FileExceptionHandler exceptionHandler; |
| |
| setUp(() { |
| windowsPlatform = FakePlatform(operatingSystem: 'windows'); |
| exceptionHandler = FileExceptionHandler(); |
| fileSystem = MemoryFileSystem.test(opHandle: exceptionHandler.opHandle); |
| }); |
| |
| testUsingContext('$CleanCommand prints a helpful error message on Windows', () async { |
| xcodeProjectInterpreter.isInstalled = false; |
| |
| final File file = fileSystem.file('file')..createSync(); |
| exceptionHandler.addError( |
| file, |
| FileSystemOp.delete, |
| const FileSystemException('Deletion failed'), |
| ); |
| |
| final CleanCommand command = CleanCommand(); |
| command.deleteFile(file); |
| expect(testLogger.errorText, contains('A program may still be using a file')); |
| }, overrides: <Type, Generator>{ |
| Platform: () => windowsPlatform, |
| Xcode: () => xcode, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('$CleanCommand handles missing delete permissions', () async { |
| final FileExceptionHandler handler = FileExceptionHandler(); |
| final FileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle); |
| final File throwingFile = fileSystem.file('bad') |
| ..createSync(); |
| handler.addError(throwingFile, FileSystemOp.delete, const FileSystemException('OS error: Access Denied')); |
| |
| xcodeProjectInterpreter.isInstalled = false; |
| |
| final CleanCommand command = CleanCommand(); |
| command.deleteFile(throwingFile); |
| |
| expect(testLogger.errorText, contains('Failed to remove bad. A program may still be using a file in the directory or the directory itself')); |
| expect(throwingFile, exists); |
| }, overrides: <Type, Generator>{ |
| Platform: () => windowsPlatform, |
| Xcode: () => xcode, |
| }); |
| }); |
| }); |
| } |
| |
| FlutterProject setupProjectUnderTest(Directory currentDirectory) { |
| // This needs to be run within testWithoutContext and not setUp since FlutterProject uses context. |
| final FlutterProject projectUnderTest = FlutterProject.fromDirectory(currentDirectory); |
| projectUnderTest.ios.xcodeWorkspace.createSync(recursive: true); |
| projectUnderTest.macos.xcodeWorkspace.createSync(recursive: true); |
| |
| projectUnderTest.dartTool.createSync(recursive: true); |
| projectUnderTest.packagesFile.createSync(recursive: true); |
| projectUnderTest.android.ephemeralDirectory.createSync(recursive: true); |
| |
| projectUnderTest.ios.ephemeralDirectory.createSync(recursive: true); |
| projectUnderTest.ios.ephemeralModuleDirectory.createSync(recursive: true); |
| projectUnderTest.ios.generatedXcodePropertiesFile.createSync(recursive: true); |
| projectUnderTest.ios.generatedEnvironmentVariableExportScript.createSync(recursive: true); |
| projectUnderTest.ios.deprecatedCompiledDartFramework.createSync(recursive: true); |
| projectUnderTest.ios.deprecatedProjectFlutterFramework.createSync(recursive: true); |
| projectUnderTest.ios.flutterPodspec.createSync(recursive: true); |
| |
| projectUnderTest.linux.ephemeralDirectory.createSync(recursive: true); |
| projectUnderTest.macos.ephemeralDirectory.createSync(recursive: true); |
| projectUnderTest.windows.ephemeralDirectory.createSync(recursive: true); |
| projectUnderTest.flutterPluginsFile.createSync(recursive: true); |
| projectUnderTest.flutterPluginsDependenciesFile.createSync(recursive: true); |
| |
| return projectUnderTest; |
| } |
| |
| class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { |
| @override |
| bool isInstalled = true; |
| |
| @override |
| Version version = Version(0, 0, 0); |
| |
| @override |
| Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async { |
| return XcodeProjectInfo( |
| const <String>[], |
| const <String>[], |
| <String>['Runner'], |
| BufferLogger.test(), |
| ); |
| } |
| |
| final List<CleanWorkspaceCall> workspaces = <CleanWorkspaceCall>[]; |
| |
| @override |
| Future<void> cleanWorkspace(String workspacePath, String scheme, {bool verbose = false}) async { |
| workspaces.add(CleanWorkspaceCall(workspacePath, scheme, verbose)); |
| return; |
| } |
| } |
| |
| @immutable |
| class CleanWorkspaceCall { |
| const CleanWorkspaceCall(this.workspacePath, this.scheme, this.verbose); |
| |
| final String workspacePath; |
| final String scheme; |
| final bool verbose; |
| |
| @override |
| bool operator ==(Object other) => other is CleanWorkspaceCall && |
| workspacePath == other.workspacePath && |
| scheme == other.scheme && |
| verbose == other.verbose; |
| |
| @override |
| int get hashCode => Object.hash(workspacePath, scheme, verbose); |
| |
| @override |
| String toString() => '{$workspacePath, $scheme, $verbose}'; |
| } |