// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:io' as io; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.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/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; class LocalFileSystemFake extends LocalFileSystem { LocalFileSystemFake.test({required super.signals}) : super.test(); @override Directory get superSystemTempDirectory => directory('/does_not_exist'); } void main() { group('fsUtils', () { late MemoryFileSystem fs; late FileSystemUtils fsUtils; setUp(() { fs = MemoryFileSystem.test(); fsUtils = FileSystemUtils( fileSystem: fs, platform: FakePlatform(), ); }); testWithoutContext('getUniqueFile creates a unique file name', () async { final File fileA = fsUtils.getUniqueFile(fs.currentDirectory, 'foo', 'json') ..createSync(); final File fileB = fsUtils.getUniqueFile(fs.currentDirectory, 'foo', 'json'); expect(fileA.path, '/foo_01.json'); expect(fileB.path, '/foo_02.json'); }); testWithoutContext('getUniqueDirectory creates a unique directory name', () async { final Directory directoryA = fsUtils.getUniqueDirectory(fs.currentDirectory, 'foo') ..createSync(); final Directory directoryB = fsUtils.getUniqueDirectory(fs.currentDirectory, 'foo'); expect(directoryA.path, '/foo_01'); expect(directoryB.path, '/foo_02'); }); }); group('copyDirectorySync', () { /// Test file_systems.copyDirectorySync() using MemoryFileSystem. /// Copies between 2 instances of file systems which is also supported by copyDirectorySync(). testWithoutContext('test directory copy', () async { final MemoryFileSystem sourceMemoryFs = MemoryFileSystem.test(); const String sourcePath = '/some/origin'; final Directory sourceDirectory = await sourceMemoryFs.directory(sourcePath).create(recursive: true); sourceMemoryFs.currentDirectory = sourcePath; final File sourceFile1 = sourceMemoryFs.file('some_file.txt')..writeAsStringSync('bleh'); final DateTime writeTime = sourceFile1.lastModifiedSync(); sourceMemoryFs.file('sub_dir/another_file.txt').createSync(recursive: true); sourceMemoryFs.directory('empty_directory').createSync(); // Copy to another memory file system instance. final MemoryFileSystem targetMemoryFs = MemoryFileSystem.test(); const String targetPath = '/some/non-existent/target'; final Directory targetDirectory = targetMemoryFs.directory(targetPath); copyDirectory(sourceDirectory, targetDirectory); expect(targetDirectory.existsSync(), true); targetMemoryFs.currentDirectory = targetPath; expect(targetMemoryFs.directory('empty_directory').existsSync(), true); expect(targetMemoryFs.file('sub_dir/another_file.txt').existsSync(), true); expect(targetMemoryFs.file('some_file.txt').readAsStringSync(), 'bleh'); // Assert that the copy operation hasn't modified the original file in some way. expect(sourceMemoryFs.file('some_file.txt').lastModifiedSync(), writeTime); // There's still 3 things in the original directory as there were initially. expect(sourceMemoryFs.directory(sourcePath).listSync().length, 3); }); testWithoutContext('test directory copy with followLinks: true', () async { final Signals signals = Signals.test(); final LocalFileSystem fileSystem = LocalFileSystem.test( signals: signals, ); final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_copy_directory.'); try { final String sourcePath = io.Platform.isWindows ? r'some\origin' : 'some/origin'; final Directory sourceDirectory = tempDir.childDirectory(sourcePath)..createSync(recursive: true); final File sourceFile1 = sourceDirectory.childFile('some_file.txt')..writeAsStringSync('file 1'); sourceDirectory.childLink('absolute_linked.txt').createSync(sourceFile1.absolute.path); final DateTime writeTime = sourceFile1.lastModifiedSync(); final Directory sourceSubDirectory = sourceDirectory.childDirectory('dir1').childDirectory('dir2')..createSync(recursive: true); sourceSubDirectory.childFile('another_file.txt').writeAsStringSync('file 2'); final String subdirectorySourcePath = io.Platform.isWindows ? r'dir1\dir2' : 'dir1/dir2'; sourceDirectory.childLink('relative_linked_sub_dir').createSync(subdirectorySourcePath); sourceDirectory.childDirectory('empty_directory').createSync(recursive: true); final String targetPath = io.Platform.isWindows ? r'some\non-existent\target' : 'some/non-existent/target'; final Directory targetDirectory = tempDir.childDirectory(targetPath); copyDirectory(sourceDirectory, targetDirectory); expect(targetDirectory.existsSync(), true); expect(targetDirectory.childFile('some_file.txt').existsSync(), true); expect(targetDirectory.childFile('some_file.txt').readAsStringSync(), 'file 1'); expect(targetDirectory.childFile('absolute_linked.txt').readAsStringSync(), 'file 1'); expect(targetDirectory.childLink('absolute_linked.txt').existsSync(), false); expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').existsSync(), true); expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').existsSync(), true); expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').readAsStringSync(), 'file 2'); expect(targetDirectory.childDirectory('relative_linked_sub_dir').existsSync(), true); expect(targetDirectory.childLink('relative_linked_sub_dir').existsSync(), false); expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').existsSync(), true); expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').readAsStringSync(), 'file 2'); expect(targetDirectory.childDirectory('empty_directory').existsSync(), true); // Assert that the copy operation hasn't modified the original file in some way. expect(sourceDirectory.childFile('some_file.txt').lastModifiedSync(), writeTime); // There's still 5 things in the original directory as there were initially. expect(sourceDirectory.listSync().length, 5); } finally { tryToDelete(tempDir); } }); testWithoutContext('test directory copy with followLinks: false', () async { final Signals signals = Signals.test(); final LocalFileSystem fileSystem = LocalFileSystem.test( signals: signals, ); final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_copy_directory.'); try { final String sourcePath = io.Platform.isWindows ? r'some\origin' : 'some/origin'; final Directory sourceDirectory = tempDir.childDirectory(sourcePath)..createSync(recursive: true); final File sourceFile1 = sourceDirectory.childFile('some_file.txt')..writeAsStringSync('file 1'); sourceDirectory.childLink('absolute_linked.txt').createSync(sourceFile1.absolute.path); final DateTime writeTime = sourceFile1.lastModifiedSync(); final Directory sourceSubDirectory = sourceDirectory.childDirectory('dir1').childDirectory('dir2')..createSync(recursive: true); sourceSubDirectory.childFile('another_file.txt').writeAsStringSync('file 2'); final String subdirectorySourcePath = io.Platform.isWindows ? r'dir1\dir2' : 'dir1/dir2'; sourceDirectory.childLink('relative_linked_sub_dir').createSync(subdirectorySourcePath); sourceDirectory.childDirectory('empty_directory').createSync(recursive: true); final String targetPath = io.Platform.isWindows ? r'some\non-existent\target' : 'some/non-existent/target'; final Directory targetDirectory = tempDir.childDirectory(targetPath); copyDirectory(sourceDirectory, targetDirectory, followLinks: false); expect(targetDirectory.existsSync(), true); expect(targetDirectory.childFile('some_file.txt').existsSync(), true); expect(targetDirectory.childFile('some_file.txt').readAsStringSync(), 'file 1'); expect(targetDirectory.childFile('absolute_linked.txt').readAsStringSync(), 'file 1'); expect(targetDirectory.childLink('absolute_linked.txt').existsSync(), true); expect( targetDirectory.childLink('absolute_linked.txt').targetSync(), sourceFile1.absolute.path, ); expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').existsSync(), true); expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').existsSync(), true); expect(targetDirectory.childDirectory('dir1').childDirectory('dir2').childFile('another_file.txt').readAsStringSync(), 'file 2'); expect(targetDirectory.childDirectory('relative_linked_sub_dir').existsSync(), true); expect(targetDirectory.childLink('relative_linked_sub_dir').existsSync(), true); expect( targetDirectory.childLink('relative_linked_sub_dir').targetSync(), subdirectorySourcePath, ); expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').existsSync(), true); expect(targetDirectory.childDirectory('relative_linked_sub_dir').childFile('another_file.txt').readAsStringSync(), 'file 2'); expect(targetDirectory.childDirectory('empty_directory').existsSync(), true); // Assert that the copy operation hasn't modified the original file in some way. expect(sourceDirectory.childFile('some_file.txt').lastModifiedSync(), writeTime); // There's still 5 things in the original directory as there were initially. expect(sourceDirectory.listSync().length, 5); } finally { tryToDelete(tempDir); } }); testWithoutContext('Skip files if shouldCopyFile returns false', () { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final Directory origin = fileSystem.directory('/origin'); origin.createSync(); fileSystem.file(fileSystem.path.join('origin', 'a.txt')).writeAsStringSync('irrelevant'); fileSystem.directory('/origin/nested').createSync(); fileSystem.file(fileSystem.path.join('origin', 'nested', 'a.txt')).writeAsStringSync('irrelevant'); fileSystem.file(fileSystem.path.join('origin', 'nested', 'b.txt')).writeAsStringSync('irrelevant'); final Directory destination = fileSystem.directory('/destination'); copyDirectory(origin, destination, shouldCopyFile: (File origin, File dest) { return origin.basename == 'b.txt'; }); expect(destination.existsSync(), isTrue); expect(destination.childDirectory('nested').existsSync(), isTrue); expect(destination.childDirectory('nested').childFile('b.txt').existsSync(), isTrue); expect(destination.childFile('a.txt').existsSync(), isFalse); expect(destination.childDirectory('nested').childFile('a.txt').existsSync(), isFalse); }); testWithoutContext('Skip directories if shouldCopyDirectory returns false', () { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final Directory origin = fileSystem.directory('/origin'); origin.createSync(); fileSystem.file(fileSystem.path.join('origin', 'a.txt')).writeAsStringSync('irrelevant'); fileSystem.directory('/origin/nested').createSync(); fileSystem.file(fileSystem.path.join('origin', 'nested', 'a.txt')).writeAsStringSync('irrelevant'); fileSystem.file(fileSystem.path.join('origin', 'nested', 'b.txt')).writeAsStringSync('irrelevant'); final Directory destination = fileSystem.directory('/destination'); copyDirectory(origin, destination, shouldCopyDirectory: (Directory directory) { return !directory.path.endsWith('nested'); }); expect(destination, exists); expect(destination.childDirectory('nested'), isNot(exists)); expect(destination.childDirectory('nested').childFile('b.txt'),isNot(exists)); }); }); group('escapePath', () { testWithoutContext('on Windows', () { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final FileSystemUtils fsUtils = FileSystemUtils( fileSystem: fileSystem, platform: FakePlatform(operatingSystem: 'windows'), ); expect(fsUtils.escapePath(r'C:\foo\bar\cool.dart'), r'C:\\foo\\bar\\cool.dart'); expect(fsUtils.escapePath(r'foo\bar\cool.dart'), r'foo\\bar\\cool.dart'); expect(fsUtils.escapePath('C:/foo/bar/cool.dart'), 'C:/foo/bar/cool.dart'); }); testWithoutContext('on Linux', () { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final FileSystemUtils fsUtils = FileSystemUtils( fileSystem: fileSystem, platform: FakePlatform(), ); expect(fsUtils.escapePath('/foo/bar/cool.dart'), '/foo/bar/cool.dart'); expect(fsUtils.escapePath('foo/bar/cool.dart'), 'foo/bar/cool.dart'); expect(fsUtils.escapePath(r'foo\cool.dart'), r'foo\cool.dart'); }); }); group('LocalFileSystem', () { late FakeProcessSignal fakeSignal; late ProcessSignal signalUnderTest; setUp(() { fakeSignal = FakeProcessSignal(); signalUnderTest = ProcessSignal(fakeSignal); }); testWithoutContext('runs shutdown hooks', () async { final Signals signals = Signals.test(); final LocalFileSystem localFileSystem = LocalFileSystem.test( signals: signals, ); final Directory temp = localFileSystem.systemTempDirectory; expect(temp.existsSync(), isTrue); expect(localFileSystem.shutdownHooks.registeredHooks, hasLength(1)); final BufferLogger logger = BufferLogger.test(); await localFileSystem.shutdownHooks.runShutdownHooks(logger); expect(temp.existsSync(), isFalse); expect(logger.traceText, contains('Running 1 shutdown hook')); }); testWithoutContext('deletes system temp entry on a fatal signal', () async { final Completer completer = Completer(); final Signals signals = Signals.test(); final LocalFileSystem localFileSystem = LocalFileSystem.test( signals: signals, fatalSignals: [signalUnderTest], ); final Directory temp = localFileSystem.systemTempDirectory; signals.addHandler(signalUnderTest, (ProcessSignal s) { completer.complete(); }); expect(temp.existsSync(), isTrue); fakeSignal.controller.add(fakeSignal); await completer.future; expect(temp.existsSync(), isFalse); }); testWithoutContext('throwToolExit when temp not found', () async { final Signals signals = Signals.test(); final LocalFileSystemFake localFileSystem = LocalFileSystemFake.test( signals: signals, ); try { localFileSystem.systemTempDirectory; fail('expected tool exit'); } on ToolExit catch (e) { expect(e.message, 'Your system temp directory (/does_not_exist) does not exist. ' 'Did you set an invalid override in your environment? ' 'See issue https://github.com/flutter/flutter/issues/74042 for more context.' ); } }); }); } class FakeProcessSignal extends Fake implements io.ProcessSignal { final StreamController controller = StreamController(); @override Stream watch() => controller.stream; }