Unverified Commit ee12d7c3 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] check for permission issues when copying file (#63540)

parent 39be8a40
...@@ -194,6 +194,17 @@ class ErrorHandlingFile ...@@ -194,6 +194,17 @@ class ErrorHandlingFile
); );
} }
@override
RandomAccessFile openSync({FileMode mode = FileMode.read}) {
return _runSync<RandomAccessFile>(
() => delegate.openSync(
mode: mode,
),
platform: _platform,
failureMessage: 'Flutter failed to open a file at "${delegate.path}"',
);
}
@override @override
String toString() => delegate.toString(); String toString() => delegate.toString();
} }
...@@ -385,9 +396,17 @@ void _handleWindowsException(FileSystemException e, String message) { ...@@ -385,9 +396,17 @@ void _handleWindowsException(FileSystemException e, String message) {
// https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes
const int kDeviceFull = 112; const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224; const int kUserMappedSectionOpened = 1224;
const int kAccessDenied = 5;
final int errorCode = e.osError?.errorCode ?? 0; final int errorCode = e.osError?.errorCode ?? 0;
// Catch errors and bail when: // Catch errors and bail when:
switch (errorCode) { switch (errorCode) {
case kAccessDenied:
throwToolExit(
'$message. The flutter tool cannot access the file.\n'
'Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.'
);
break;
case kDeviceFull: case kDeviceFull:
throwToolExit( throwToolExit(
'$message. The target device is full.' '$message. The target device is full.'
......
...@@ -32,18 +32,19 @@ import '../runner/flutter_command.dart'; ...@@ -32,18 +32,19 @@ import '../runner/flutter_command.dart';
import '../template.dart'; import '../template.dart';
const List<String> _kAvailablePlatforms = <String>[ const List<String> _kAvailablePlatforms = <String>[
'ios', 'ios',
'android', 'android',
'windows', 'windows',
'linux', 'linux',
'macos', 'macos',
'web', 'web',
]; ];
const String _kNoPlatformsErrorMessage = ''' const String _kNoPlatformsErrorMessage = '''
The plugin project was generated without specifying the `--platforms` flag, no new platforms are added. The plugin project was generated without specifying the `--platforms` flag, no new platforms are added.
To add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same To add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same
directory. You can also find detailed instructions on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. directory. You can also find detailed instructions on how to add platforms in the `pubspec.yaml`
at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
'''; ''';
class CreateCommand extends FlutterCommand { class CreateCommand extends FlutterCommand {
...@@ -790,7 +791,10 @@ https://flutter.dev/docs/development/packages-and-plugins/developing-packages#pl ...@@ -790,7 +791,10 @@ https://flutter.dev/docs/development/packages-and-plugins/developing-packages#pl
final Template template = await Template.fromName( final Template template = await Template.fromName(
templateName, templateName,
fileSystem: globals.fs, fileSystem: globals.fs,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
templateManifest: templateManifest, templateManifest: templateManifest,
pub: pub,
); );
return template.render(directory, context, overwriteExisting: overwrite); return template.render(directory, context, overwriteExisting: overwrite);
} }
......
...@@ -254,6 +254,8 @@ class IdeConfigCommand extends FlutterCommand { ...@@ -254,6 +254,8 @@ class IdeConfigCommand extends FlutterCommand {
null, null,
fileSystem: globals.fs, fileSystem: globals.fs,
templateManifest: null, templateManifest: null,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
); );
return template.render( return template.render(
globals.fs.directory(dirPath), globals.fs.directory(dirPath),
......
...@@ -15,6 +15,7 @@ import 'base/file_system.dart'; ...@@ -15,6 +15,7 @@ import 'base/file_system.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'bundle.dart' as bundle; import 'bundle.dart' as bundle;
import 'dart/pub.dart';
import 'features.dart'; import 'features.dart';
import 'flutter_manifest.dart'; import 'flutter_manifest.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
...@@ -657,7 +658,14 @@ class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { ...@@ -657,7 +658,14 @@ class IosProject extends FlutterProjectPlatform implements XcodeBasedProject {
} }
Future<void> _overwriteFromTemplate(String path, Directory target) async { Future<void> _overwriteFromTemplate(String path, Directory target) async {
final Template template = await Template.fromName(path, fileSystem: globals.fs, templateManifest: null); final Template template = await Template.fromName(
path,
fileSystem: globals.fs,
templateManifest: null,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
pub: pub,
);
template.render( template.render(
target, target,
<String, dynamic>{ <String, dynamic>{
...@@ -810,7 +818,14 @@ to migrate your project. ...@@ -810,7 +818,14 @@ to migrate your project.
} }
Future<void> _overwriteFromTemplate(String path, Directory target) async { Future<void> _overwriteFromTemplate(String path, Directory target) async {
final Template template = await Template.fromName(path, fileSystem: globals.fs, templateManifest: null); final Template template = await Template.fromName(
path,
fileSystem: globals.fs,
templateManifest: null,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
pub: pub,
);
template.render( template.render(
target, target,
<String, dynamic>{ <String, dynamic>{
......
...@@ -8,10 +8,11 @@ import 'package:package_config/package_config_types.dart'; ...@@ -8,10 +8,11 @@ import 'package:package_config/package_config_types.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/template.dart';
import 'cache.dart'; import 'cache.dart';
import 'dart/package_map.dart'; import 'dart/package_map.dart';
import 'dart/pub.dart'; import 'dart/pub.dart';
import 'globals.dart' as globals hide fs;
/// Expands templates in a directory to a destination. All files that must /// Expands templates in a directory to a destination. All files that must
/// undergo template expansion should end with the '.tmpl' extension. All files /// undergo template expansion should end with the '.tmpl' extension. All files
...@@ -32,8 +33,12 @@ import 'globals.dart' as globals hide fs; ...@@ -32,8 +33,12 @@ import 'globals.dart' as globals hide fs;
class Template { class Template {
Template(Directory templateSource, Directory baseDir, this.imageSourceDir, { Template(Directory templateSource, Directory baseDir, this.imageSourceDir, {
@required FileSystem fileSystem, @required FileSystem fileSystem,
@required Logger logger,
@required TemplateRenderer templateRenderer,
@required Set<Uri> templateManifest, @required Set<Uri> templateManifest,
}) : _fileSystem = fileSystem, }) : _fileSystem = fileSystem,
_logger = logger,
_templateRenderer = templateRenderer,
_templateManifest = templateManifest { _templateManifest = templateManifest {
_templateFilePaths = <String, String>{}; _templateFilePaths = <String, String>{};
...@@ -42,14 +47,9 @@ class Template { ...@@ -42,14 +47,9 @@ class Template {
} }
final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true); final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true);
for (final FileSystemEntity entity in templateFiles.whereType<File>()) {
for (final FileSystemEntity entity in templateFiles) {
if (entity is! File) {
// We are only interesting in template *file* URIs.
continue;
}
if (_templateManifest != null && !_templateManifest.contains(Uri.file(entity.absolute.path))) { if (_templateManifest != null && !_templateManifest.contains(Uri.file(entity.absolute.path))) {
globals.logger.printTrace('Skipping ${entity.absolute.path}, missing from the template manifest.'); _logger.printTrace('Skipping ${entity.absolute.path}, missing from the template manifest.');
// Skip stale files in the flutter_tools directory. // Skip stale files in the flutter_tools directory.
continue; continue;
} }
...@@ -68,20 +68,28 @@ class Template { ...@@ -68,20 +68,28 @@ class Template {
static Future<Template> fromName(String name, { static Future<Template> fromName(String name, {
@required FileSystem fileSystem, @required FileSystem fileSystem,
@required Set<Uri> templateManifest, @required Set<Uri> templateManifest,
@required Logger logger,
@required TemplateRenderer templateRenderer,
@required Pub pub,
}) async { }) async {
// All named templates are placed in the 'templates' directory // All named templates are placed in the 'templates' directory
final Directory templateDir = _templateDirectoryInPackage(name, fileSystem); final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
final Directory imageDir = await _templateImageDirectory(name, fileSystem); final Directory imageDir = await _templateImageDirectory(name, fileSystem, logger, pub);
return Template( return Template(
templateDir, templateDir,
templateDir, imageDir, templateDir, imageDir,
fileSystem: fileSystem, fileSystem: fileSystem,
logger: logger,
templateRenderer: templateRenderer,
templateManifest: templateManifest, templateManifest: templateManifest,
); );
} }
final FileSystem _fileSystem; final FileSystem _fileSystem;
final Logger _logger;
final Set<Uri> _templateManifest; final Set<Uri> _templateManifest;
final TemplateRenderer _templateRenderer;
static const String templateExtension = '.tmpl'; static const String templateExtension = '.tmpl';
static const String copyTemplateExtension = '.copy.tmpl'; static const String copyTemplateExtension = '.copy.tmpl';
static const String imageTemplateExtension = '.img.tmpl'; static const String imageTemplateExtension = '.img.tmpl';
...@@ -102,7 +110,7 @@ class Template { ...@@ -102,7 +110,7 @@ class Template {
try { try {
destination.createSync(recursive: true); destination.createSync(recursive: true);
} on FileSystemException catch (err) { } on FileSystemException catch (err) {
globals.printError(err.toString()); _logger.printError(err.toString());
throwToolExit('Failed to flutter create at ${destination.path}.'); throwToolExit('Failed to flutter create at ${destination.path}.');
return 0; return 0;
} }
...@@ -198,22 +206,22 @@ class Template { ...@@ -198,22 +206,22 @@ class Template {
if (overwriteExisting) { if (overwriteExisting) {
finalDestinationFile.deleteSync(recursive: true); finalDestinationFile.deleteSync(recursive: true);
if (printStatusWhenWriting) { if (printStatusWhenWriting) {
globals.printStatus(' $relativePathForLogging (overwritten)'); _logger.printStatus(' $relativePathForLogging (overwritten)');
} }
} else { } else {
// The file exists but we cannot overwrite it, move on. // The file exists but we cannot overwrite it, move on.
if (printStatusWhenWriting) { if (printStatusWhenWriting) {
globals.printTrace(' $relativePathForLogging (existing - skipped)'); _logger.printTrace(' $relativePathForLogging (existing - skipped)');
} }
return; return;
} }
} else { } else {
if (printStatusWhenWriting) { if (printStatusWhenWriting) {
globals.printStatus(' $relativePathForLogging (created)'); _logger.printStatus(' $relativePathForLogging (created)');
} }
} }
fileCount++; fileCount += 1;
finalDestinationFile.createSync(recursive: true); finalDestinationFile.createSync(recursive: true);
final File sourceFile = _fileSystem.file(absoluteSourcePath); final File sourceFile = _fileSystem.file(absoluteSourcePath);
...@@ -222,6 +230,7 @@ class Template { ...@@ -222,6 +230,7 @@ class Template {
// not need mustache rendering but needs to be directly copied. // not need mustache rendering but needs to be directly copied.
if (sourceFile.path.endsWith(copyTemplateExtension)) { if (sourceFile.path.endsWith(copyTemplateExtension)) {
_validateReadPermissions(sourceFile);
sourceFile.copySync(finalDestinationFile.path); sourceFile.copySync(finalDestinationFile.path);
return; return;
...@@ -233,6 +242,7 @@ class Template { ...@@ -233,6 +242,7 @@ class Template {
if (sourceFile.path.endsWith(imageTemplateExtension)) { if (sourceFile.path.endsWith(imageTemplateExtension)) {
final File imageSourceFile = _fileSystem.file(_fileSystem.path.join( final File imageSourceFile = _fileSystem.file(_fileSystem.path.join(
imageSourceDir.path, relativeDestinationPath.replaceAll(imageTemplateExtension, ''))); imageSourceDir.path, relativeDestinationPath.replaceAll(imageTemplateExtension, '')));
_validateReadPermissions(imageSourceFile);
imageSourceFile.copySync(finalDestinationFile.path); imageSourceFile.copySync(finalDestinationFile.path);
return; return;
...@@ -242,8 +252,9 @@ class Template { ...@@ -242,8 +252,9 @@ class Template {
// rendering via mustache. // rendering via mustache.
if (sourceFile.path.endsWith(templateExtension)) { if (sourceFile.path.endsWith(templateExtension)) {
_validateReadPermissions(sourceFile);
final String templateContents = sourceFile.readAsStringSync(); final String templateContents = sourceFile.readAsStringSync();
final String renderedContents = globals.templateRenderer.renderString(templateContents, context); final String renderedContents = _templateRenderer.renderString(templateContents, context);
finalDestinationFile.writeAsStringSync(renderedContents); finalDestinationFile.writeAsStringSync(renderedContents);
...@@ -252,12 +263,20 @@ class Template { ...@@ -252,12 +263,20 @@ class Template {
// Step 5: This file does not end in .tmpl but is in a directory that // Step 5: This file does not end in .tmpl but is in a directory that
// does. Directly copy the file to the destination. // does. Directly copy the file to the destination.
_validateReadPermissions(sourceFile);
sourceFile.copySync(finalDestinationFile.path); sourceFile.copySync(finalDestinationFile.path);
}); });
return fileCount; return fileCount;
} }
/// Attempt open/close the file to ensure that read permissions are correct.
///
/// If this fails with a certain error code, the [ErrorHandlingFileSystem] will
/// trigger a tool exit with a better message.
void _validateReadPermissions(File file) {
file.openSync().closeSync();
}
} }
Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) { Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
...@@ -268,25 +287,25 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) { ...@@ -268,25 +287,25 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
// Returns the directory containing the 'name' template directory in // Returns the directory containing the 'name' template directory in
// flutter_template_images, to resolve image placeholder against. // flutter_template_images, to resolve image placeholder against.
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem) async { Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Logger logger, Pub pub) async {
final String toolPackagePath = fileSystem.path.join( final String toolPackagePath = fileSystem.path.join(
Cache.flutterRoot, 'packages', 'flutter_tools'); Cache.flutterRoot, 'packages', 'flutter_tools');
final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName); final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName);
// Ensure that .packgaes is present. // Ensure that .packgaes is present.
if (!fileSystem.file(packageFilePath).existsSync()) { if (!fileSystem.file(packageFilePath).existsSync()) {
await _ensurePackageDependencies(toolPackagePath); await _ensurePackageDependencies(toolPackagePath, pub);
} }
PackageConfig packageConfig = await loadPackageConfigWithLogging( PackageConfig packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath), fileSystem.file(packageFilePath),
logger: globals.logger, logger: logger,
); );
Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
// Ensure that the template image package is present. // Ensure that the template image package is present.
if (imagePackageLibDir == null || !fileSystem.directory(imagePackageLibDir).existsSync()) { if (imagePackageLibDir == null || !fileSystem.directory(imagePackageLibDir).existsSync()) {
await _ensurePackageDependencies(toolPackagePath); await _ensurePackageDependencies(toolPackagePath, pub);
packageConfig = await loadPackageConfigWithLogging( packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath), fileSystem.file(packageFilePath),
logger: globals.logger, logger: logger,
); );
imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
} }
...@@ -298,7 +317,7 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem) as ...@@ -298,7 +317,7 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem) as
// Runs 'pub get' for the given path to ensure that .packages is created and // Runs 'pub get' for the given path to ensure that .packages is created and
// all dependencies are present. // all dependencies are present.
Future<void> _ensurePackageDependencies(String packagePath) async { Future<void> _ensurePackageDependencies(String packagePath, Pub pub) async {
await pub.get( await pub.get(
context: PubContext.pubGet, context: PubContext.pubGet,
directory: packagePath, directory: packagePath,
......
...@@ -59,6 +59,9 @@ void setupWriteMocks({ ...@@ -59,6 +59,9 @@ void setupWriteMocks({
encoding: anyNamed('encoding'), encoding: anyNamed('encoding'),
flush: anyNamed('flush'), flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode))); )).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.openSync(
mode: anyNamed('mode'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
} }
void setupCreateTempMocks({ void setupCreateTempMocks({
...@@ -79,6 +82,7 @@ void main() { ...@@ -79,6 +82,7 @@ void main() {
group('throws ToolExit on Windows', () { group('throws ToolExit on Windows', () {
const int kDeviceFull = 112; const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224; const int kUserMappedSectionOpened = 1224;
const int kUserPermissionDenied = 5;
MockFileSystem mockFileSystem; MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs; ErrorHandlingFileSystem fs;
...@@ -91,6 +95,28 @@ void main() { ...@@ -91,6 +95,28 @@ void main() {
when(mockFileSystem.path).thenReturn(MockPathContext()); when(mockFileSystem.path).thenReturn(MockPathContext());
}); });
testWithoutContext('when access is denied', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final File file = fs.file('file');
const String expectedMessage = 'The flutter tool cannot access the file';
expect(() async => await file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => await file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
expect(() => file.openSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when writing to a full device', () async { testWithoutContext('when writing to a full device', () async {
setupWriteMocks( setupWriteMocks(
mockFileSystem: mockFileSystem, mockFileSystem: mockFileSystem,
......
...@@ -389,3 +389,24 @@ class TestFlutterCommandRunner extends FlutterCommandRunner { ...@@ -389,3 +389,24 @@ class TestFlutterCommandRunner extends FlutterCommandRunner {
); );
} }
} }
/// A file system that allows preconfiguring certain entities.
///
/// This is useful for inserting mocks/entities which throw errors or
/// have other behavior that is not easily configured through the
/// filesystem interface.
class ConfiguredFileSystem extends ForwardingFileSystem {
ConfiguredFileSystem(FileSystem delegate, {@required this.entities}) : super(delegate);
final Map<String, FileSystemEntity> entities;
@override
File file(dynamic path) {
return (entities[path] as File) ?? super.file(path);
}
@override
Directory directory(dynamic path) {
return (entities[path] as Directory) ?? super.directory(path);
}
}
...@@ -3,29 +3,26 @@ ...@@ -3,29 +3,26 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart'; 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/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/template.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/template.dart'; import 'package:flutter_tools/src/template.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'src/common.dart'; import 'src/common.dart';
import 'src/testbed.dart';
void main() { void main() {
Testbed testbed; testWithoutContext('Template.render throws ToolExit when FileSystem exception is raised', () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
setUp(() {
testbed = Testbed();
});
test('Template.render throws ToolExit when FileSystem exception is raised', () => testbed.run(() {
final Template template = Template( final Template template = Template(
globals.fs.directory('examples'), fileSystem.directory('examples'),
globals.fs.currentDirectory, fileSystem.currentDirectory,
null, null,
fileSystem: globals.fs, fileSystem: fileSystem,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
templateManifest: null, templateManifest: null,
); );
final MockDirectory mockDirectory = MockDirectory(); final MockDirectory mockDirectory = MockDirectory();
...@@ -33,9 +30,33 @@ void main() { ...@@ -33,9 +30,33 @@ void main() {
expect(() => template.render(mockDirectory, <String, Object>{}), expect(() => template.render(mockDirectory, <String, Object>{}),
throwsToolExit()); throwsToolExit());
})); });
test('Template.render replaces .img.tmpl files with files from the image source', () => testbed.run(() { testWithoutContext('Template.render attempts to read byte from template file before copying', () {
final MemoryFileSystem baseFileSystem = MemoryFileSystem.test();
baseFileSystem.file('templates/foo.copy.tmpl').createSync(recursive: true);
final ConfiguredFileSystem fileSystem = ConfiguredFileSystem(
baseFileSystem,
entities: <String, FileSystemEntity>{
'/templates/foo.copy.tmpl': FakeFile('/templates/foo.copy.tmpl'),
},
);
final Template template = Template(
fileSystem.directory('templates'),
fileSystem.currentDirectory,
null,
fileSystem: fileSystem,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
templateManifest: null,
);
expect(() => template.render(fileSystem.directory('out'), <String, Object>{}),
throwsA(isA<FileSystemException>()));
});
testWithoutContext('Template.render replaces .img.tmpl files with files from the image source', () {
final MemoryFileSystem fileSystem = MemoryFileSystem(); final MemoryFileSystem fileSystem = MemoryFileSystem();
final Directory templateDir = fileSystem.directory('templates'); final Directory templateDir = fileSystem.directory('templates');
final Directory imageSourceDir = fileSystem.directory('template_images'); final Directory imageSourceDir = fileSystem.directory('template_images');
...@@ -52,43 +73,77 @@ void main() { ...@@ -52,43 +73,77 @@ void main() {
imageSourceDir, imageSourceDir,
fileSystem: fileSystem, fileSystem: fileSystem,
templateManifest: null, templateManifest: null,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
); );
template.render(destination, <String, Object>{}); template.render(destination, <String, Object>{});
final File destinationImage = destination.childFile(imageName); final File destinationImage = destination.childFile(imageName);
expect(destinationImage.existsSync(), true); expect(destinationImage, exists);
expect(destinationImage.readAsBytesSync(), equals(sourceImage.readAsBytesSync())); expect(destinationImage.readAsBytesSync(), equals(sourceImage.readAsBytesSync()));
})); });
test('Template.fromName runs pub get if .packages is missing', () => testbed.run(() async { testWithoutContext('Template.fromName runs pub get if .packages is missing', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem(); final MemoryFileSystem fileSystem = MemoryFileSystem();
final MockPub pub = MockPub();
when(pub.get(
context: PubContext.pubGet,
directory: anyNamed('directory'),
generateSyntheticPackage: false,
)).thenThrow(UnsupportedError(''));
Cache.flutterRoot = '/flutter';
// Attempting to run pub in a test throws. // Attempting to run pub in a test throws.
await expectLater(Template.fromName('app', fileSystem: fileSystem, templateManifest: null), await expectLater(Template.fromName(
throwsUnsupportedError); 'app',
})); fileSystem: fileSystem,
templateManifest: null,
logger: BufferLogger.test(),
pub: pub,
templateRenderer: FakeTemplateRenderer(),
),
throwsUnsupportedError,
);
});
test('Template.fromName runs pub get if .packages is missing flutter_template_images', () => testbed.run(() async { testWithoutContext('Template.fromName runs pub get if .packages is missing flutter_template_images', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem(); final MemoryFileSystem fileSystem = MemoryFileSystem();
final MockPub pub = MockPub();
when(pub.get(
context: PubContext.pubGet,
directory: anyNamed('directory'),
generateSyntheticPackage: false,
)).thenThrow(UnsupportedError(''));
Cache.flutterRoot = '/flutter'; Cache.flutterRoot = '/flutter';
final File packagesFile = fileSystem.directory(Cache.flutterRoot) final File packagesFile = fileSystem.directory(Cache.flutterRoot)
.childDirectory('packages') .childDirectory('packages')
.childDirectory('flutter_tools') .childDirectory('flutter_tools')
.childFile('.packages'); .childFile('.packages');
packagesFile.createSync(recursive: true); packagesFile.createSync(recursive: true);
// Attempting to run pub in a test throws. // Attempting to run pub in a test throws.
await expectLater(Template.fromName('app', fileSystem: fileSystem, templateManifest: null), await expectLater(
throwsUnsupportedError); Template.fromName(
})); 'app',
fileSystem: fileSystem,
templateManifest: null,
logger: BufferLogger.test(),
pub: pub,
templateRenderer: FakeTemplateRenderer(),
),
throwsUnsupportedError,
);
});
test('Template.fromName runs pub get if flutter_template_images directory is missing', () => testbed.run(() async { testWithoutContext('Template.fromName runs pub get if flutter_template_images directory is missing', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem(); final MemoryFileSystem fileSystem = MemoryFileSystem();
final MockPub pub = MockPub();
Cache.flutterRoot = '/flutter'; Cache.flutterRoot = '/flutter';
final File packagesFile = fileSystem.directory(Cache.flutterRoot) final File packagesFile = fileSystem.directory(Cache.flutterRoot)
.childDirectory('packages') .childDirectory('packages')
.childDirectory('flutter_tools') .childDirectory('flutter_tools')
.childFile('.packages'); .childFile('.packages');
packagesFile.createSync(recursive: true); packagesFile.createSync(recursive: true);
packagesFile.writeAsStringSync('\n'); packagesFile.writeAsStringSync('\n');
...@@ -101,11 +156,35 @@ void main() { ...@@ -101,11 +156,35 @@ void main() {
packagesFile.writeAsStringSync('flutter_template_images:file:///flutter_template_images'); packagesFile.writeAsStringSync('flutter_template_images:file:///flutter_template_images');
}); });
await Template.fromName('app', fileSystem: fileSystem, templateManifest: null); await Template.fromName(
}, overrides: <Type, Generator>{ 'app',
Pub: () => MockPub(), fileSystem: fileSystem,
})); templateManifest: null,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
pub: pub,
);
});
} }
class MockPub extends Mock implements Pub {} class MockPub extends Mock implements Pub {}
class MockDirectory extends Mock implements Directory {} class MockDirectory extends Mock implements Directory {}
class FakeFile extends Fake implements File {
FakeFile(this.path);
@override
final String path;
@override
int lengthSync() {
throw const FileSystemException('', '', OSError('', 5));
}
}
class FakeTemplateRenderer extends TemplateRenderer {
@override
String renderString(String template, dynamic context, {bool htmlEscapeValues = false}) {
return '';
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment