blob: b7a6424f97be8745b734116c361cacfa588cbb14 [file] [log] [blame]
// Copyright 2013 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_migrate/src/base/common.dart';
import 'package:flutter_migrate/src/base/file_system.dart';
import 'package:flutter_migrate/src/base/io.dart';
import 'package:flutter_migrate/src/base/logger.dart';
import 'package:flutter_migrate/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,
);
});
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('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('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<void> completer = Completer<void>();
final Signals signals = Signals.test();
final LocalFileSystem localFileSystem = LocalFileSystem.test(
signals: signals,
fatalSignals: <ProcessSignal>[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<io.ProcessSignal> controller =
StreamController<io.ProcessSignal>();
@override
Stream<io.ProcessSignal> watch() => controller.stream;
}
/// Various convenience file system methods.
class FileSystemUtils {
FileSystemUtils({
required FileSystem fileSystem,
}) : _fileSystem = fileSystem;
final FileSystem _fileSystem;
/// Appends a number to a filename in order to make it unique under a
/// directory.
File getUniqueFile(Directory dir, String baseName, String ext) {
final FileSystem fs = dir.fileSystem;
int i = 1;
while (true) {
final String name = '${baseName}_${i.toString().padLeft(2, '0')}.$ext';
final File file = fs.file(dir.fileSystem.path.join(dir.path, name));
if (!file.existsSync()) {
file.createSync(recursive: true);
return file;
}
i += 1;
}
}
// /// Appends a number to a filename in order to make it unique under a
// /// directory.
// File getUniqueFile(Directory dir, String baseName, String ext) {
// return _getUniqueFile(dir, baseName, ext);
// }
/// Appends a number to a directory name in order to make it unique under a
/// directory.
Directory getUniqueDirectory(Directory dir, String baseName) {
final FileSystem fs = dir.fileSystem;
int i = 1;
while (true) {
final String name = '${baseName}_${i.toString().padLeft(2, '0')}';
final Directory directory =
fs.directory(_fileSystem.path.join(dir.path, name));
if (!directory.existsSync()) {
return directory;
}
i += 1;
}
}
/// Escapes [path].
///
/// On Windows it replaces all '\' with '\\'. On other platforms, it returns the
/// path unchanged.
String escapePath(String path) =>
isWindows ? path.replaceAll(r'\', r'\\') : path;
/// Returns true if the file system [entity] has not been modified since the
/// latest modification to [referenceFile].
///
/// Returns true, if [entity] does not exist.
///
/// Returns false, if [entity] exists, but [referenceFile] does not.
bool isOlderThanReference({
required FileSystemEntity entity,
required File referenceFile,
}) {
if (!entity.existsSync()) {
return true;
}
return referenceFile.existsSync() &&
referenceFile.statSync().modified.isAfter(entity.statSync().modified);
}
}
/// Creates `destDir` if needed, then recursively copies `srcDir` to
/// `destDir`, invoking [onFileCopied], if specified, for each
/// source/destination file pair.
///
/// Skips files if [shouldCopyFile] returns `false`.
/// Does not recurse over directories if [shouldCopyDirectory] returns `false`.
void copyDirectory(
Directory srcDir,
Directory destDir, {
bool Function(File srcFile, File destFile)? shouldCopyFile,
bool Function(Directory)? shouldCopyDirectory,
void Function(File srcFile, File destFile)? onFileCopied,
}) {
if (!srcDir.existsSync()) {
throw Exception(
'Source directory "${srcDir.path}" does not exist, nothing to copy');
}
if (!destDir.existsSync()) {
destDir.createSync(recursive: true);
}
for (final FileSystemEntity entity in srcDir.listSync()) {
final String newPath =
destDir.fileSystem.path.join(destDir.path, entity.basename);
if (entity is Link) {
final Link newLink = destDir.fileSystem.link(newPath);
newLink.createSync(entity.targetSync());
} else if (entity is File) {
final File newFile = destDir.fileSystem.file(newPath);
if (shouldCopyFile != null && !shouldCopyFile(entity, newFile)) {
continue;
}
newFile.writeAsBytesSync(entity.readAsBytesSync());
onFileCopied?.call(entity, newFile);
} else if (entity is Directory) {
if (shouldCopyDirectory != null && !shouldCopyDirectory(entity)) {
continue;
}
copyDirectory(
entity,
destDir.fileSystem.directory(newPath),
shouldCopyFile: shouldCopyFile,
onFileCopied: onFileCopied,
);
} else {
throw Exception(
'${entity.path} is neither File nor Directory, was ${entity.runtimeType}');
}
}
}