From 28b6a1f7ea81cf1a5ea78e940f8a17f61c0684e6 Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Thu, 3 Nov 2016 08:21:10 -0700 Subject: [PATCH] Add implementations of the front end FileSystem API. This required some small changes to the API contract. Note that the tests use supermixins. I'm assuming that supermixin functionality will be available on all platforms by the time this is needed. If not, I will be happy to rewrite them. R=scheglov@google.com, sigmund@google.com Review URL: https://codereview.chromium.org/2471283002 . --- .packages | 1 + pkg/front_end/lib/file_system.dart | 12 +- pkg/front_end/lib/memory_file_system.dart | 100 +++++++ pkg/front_end/lib/physical_file_system.dart | 59 ++++ pkg/front_end/pubspec.yaml | 4 +- .../test/memory_file_system_test.dart | 259 ++++++++++++++++++ .../test/physical_file_system_test.dart | 212 ++++++++++++++ pkg/pkg.status | 2 + 8 files changed, 643 insertions(+), 6 deletions(-) create mode 100644 pkg/front_end/lib/memory_file_system.dart create mode 100644 pkg/front_end/lib/physical_file_system.dart create mode 100644 pkg/front_end/test/memory_file_system_test.dart create mode 100644 pkg/front_end/test/physical_file_system_test.dart diff --git a/.packages b/.packages index 4dbf1a4a36db..3c252bb166d8 100644 --- a/.packages +++ b/.packages @@ -35,6 +35,7 @@ dartdoc:third_party/pkg/dartdoc/lib dev_compiler:pkg/dev_compiler/lib expect:pkg/expect/lib fixnum:third_party/pkg/fixnum/lib +front_end:pkg/front_end/lib func:third_party/pkg/func/lib glob:third_party/pkg/glob/lib html:third_party/pkg/html/lib diff --git a/pkg/front_end/lib/file_system.dart b/pkg/front_end/lib/file_system.dart index 404f7f617eca..cadf82fddf40 100644 --- a/pkg/front_end/lib/file_system.dart +++ b/pkg/front_end/lib/file_system.dart @@ -31,10 +31,9 @@ abstract class FileSystem { /// Returns a [FileSystemEntity] corresponding to the given [uri]. /// - /// Uses of `..` and `.` in the URI are normalized before returning. Relative - /// paths are also converted to absolute paths. + /// Uses of `..` and `.` in the URI are normalized before returning. /// - /// If [uri] is not a `file:` URI, an [Error] will be thrown. + /// If [uri] is not an absolute `file:` URI, an [Error] will be thrown. /// /// Does not check whether a file or folder exists at the given location. FileSystemEntity entityForUri(Uri uri); @@ -42,6 +41,9 @@ abstract class FileSystem { /// Abstract representation of a file system entity that may or may not exist. /// +/// Instances of this class have suitable implementations of equality tests and +/// hashCode. +/// /// Not intended to be implemented or extended by clients. abstract class FileSystemEntity { /// Returns the absolute normalized path represented by this file system @@ -67,7 +69,7 @@ abstract class FileSystemEntity { /// The file is assumed to be UTF-8 encoded. /// /// If an error occurs while attempting to read the file (e.g. because no such - /// file exists, or the entity is a directory), the future is completed with - /// an [Exception]. + /// file exists, the entity is a directory, or the file is not valid UTF-8), + /// the future is completed with an [Exception]. Future readAsString(); } diff --git a/pkg/front_end/lib/memory_file_system.dart b/pkg/front_end/lib/memory_file_system.dart new file mode 100644 index 000000000000..267cbe36a6a7 --- /dev/null +++ b/pkg/front_end/lib/memory_file_system.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library front_end.memory_file_system; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; + +import 'file_system.dart'; + +/// Concrete implementation of [FileSystem] which performs its operations on an +/// in-memory virtual file system. +/// +/// Not intended to be implemented or extended by clients. +class MemoryFileSystem implements FileSystem { + @override + final p.Context context; + + final Map _files = {}; + + /// The "current directory" in the in-memory virtual file system. + /// + /// This is used to convert relative paths to absolute paths. + String currentDirectory; + + MemoryFileSystem(this.context, this.currentDirectory); + + @override + MemoryFileSystemEntity entityForPath(String path) => + new MemoryFileSystemEntity._( + this, context.normalize(context.join(currentDirectory, path))); + + @override + MemoryFileSystemEntity entityForUri(Uri uri) { + if (uri.scheme != 'file') throw new ArgumentError('File URI expected'); + // Note: we don't have to verify that the URI's path is absolute, because + // URIs with non-empty schemes always have absolute paths. + return entityForPath(context.fromUri(uri)); + } +} + +/// Concrete implementation of [FileSystemEntity] for use by +/// [MemoryFileSystem]. +class MemoryFileSystemEntity implements FileSystemEntity { + final MemoryFileSystem _fileSystem; + + @override + final String path; + + MemoryFileSystemEntity._(this._fileSystem, this.path); + + @override + int get hashCode => path.hashCode; + + @override + bool operator ==(Object other) => + other is MemoryFileSystemEntity && + other.path == path && + identical(other._fileSystem, _fileSystem); + + @override + Future> readAsBytes() async { + List contents = _fileSystem._files[path]; + if (contents != null) { + return contents.toList(); + } + throw new Exception('File does not exist'); + } + + @override + Future readAsString() async { + List contents = await readAsBytes(); + return UTF8.decode(contents); + } + + /// Writes the given raw bytes to this file system entity. + /// + /// If no file exists, one is created. If a file exists already, it is + /// overwritten. + void writeAsBytesSync(List bytes) { + _fileSystem._files[path] = new Uint8List.fromList(bytes); + } + + /// Writes the given string to this file system entity. + /// + /// The string is encoded as UTF-8. + /// + /// If no file exists, one is created. If a file exists already, it is + /// overwritten. + void writeAsStringSync(String s) { + // Note: the return type of UTF8.encode is List, but in practice it + // always returns Uint8List. We rely on that for efficiency, so that we + // don't have to make an extra copy. + _fileSystem._files[path] = UTF8.encode(s) as Uint8List; + } +} diff --git a/pkg/front_end/lib/physical_file_system.dart b/pkg/front_end/lib/physical_file_system.dart new file mode 100644 index 000000000000..0ed5d8b0926d --- /dev/null +++ b/pkg/front_end/lib/physical_file_system.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library front_end.physical_file_system; + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:path/path.dart' as p; + +import 'file_system.dart'; + +/// Concrete implementation of [FileSystem] which performs its operations using +/// I/O. +/// +/// Not intended to be implemented or extended by clients. +class PhysicalFileSystem implements FileSystem { + static final PhysicalFileSystem instance = new PhysicalFileSystem._(); + + PhysicalFileSystem._(); + + @override + p.Context get context => p.context; + + @override + FileSystemEntity entityForPath(String path) => + new _PhysicalFileSystemEntity(context.normalize(context.absolute(path))); + + @override + FileSystemEntity entityForUri(Uri uri) { + if (uri.scheme != 'file') throw new ArgumentError('File URI expected'); + // Note: we don't have to verify that the URI's path is absolute, because + // URIs with non-empty schemes always have absolute paths. + return entityForPath(context.fromUri(uri)); + } +} + +/// Concrete implementation of [FileSystemEntity] for use by +/// [PhysicalFileSystem]. +class _PhysicalFileSystemEntity implements FileSystemEntity { + @override + final String path; + + _PhysicalFileSystemEntity(this.path); + + @override + int get hashCode => path.hashCode; + + @override + bool operator ==(Object other) => + other is _PhysicalFileSystemEntity && other.path == path; + + @override + Future> readAsBytes() => new io.File(path).readAsBytes(); + + @override + Future readAsString() => new io.File(path).readAsString(); +} diff --git a/pkg/front_end/pubspec.yaml b/pkg/front_end/pubspec.yaml index 9c09fa2e99da..58c4bb1555b2 100644 --- a/pkg/front_end/pubspec.yaml +++ b/pkg/front_end/pubspec.yaml @@ -10,10 +10,12 @@ dependencies: path: '^1.3.9' source_span: '^1.2.3' dev_dependencies: - package_config: '^1.0.0' # TODO(sigmund): update to a version constraint once we roll the latest kernel # to the repo. kernel: {path: ../../third_party/pkg/kernel} + package_config: '^1.0.0' + test: ^0.12.0 + test_reflective_loader: ^0.1.0 # TODO(sigmund): remove once kernel is moved into the sdk repo. dependency_overrides: analyzer: '^0.29.0' diff --git a/pkg/front_end/test/memory_file_system_test.dart b/pkg/front_end/test/memory_file_system_test.dart new file mode 100644 index 000000000000..b9757f3926a9 --- /dev/null +++ b/pkg/front_end/test/memory_file_system_test.dart @@ -0,0 +1,259 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// SharedOptions=--supermixin + +library front_end.test.memory_file_system_test; + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:front_end/memory_file_system.dart'; +import 'package:path/path.dart' as pathos; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(MemoryFileSystemTestNative); + defineReflectiveTests(MemoryFileSystemTestPosix); + defineReflectiveTests(MemoryFileSystemTestWindows); + defineReflectiveTests(FileTest); + }); +} + +@reflectiveTest +class FileTest extends _BaseTestNative { + String path; + MemoryFileSystemEntity file; + + setUp() { + super.setUp(); + path = join(tempPath, 'file.txt'); + file = fileSystem.entityForPath(path); + } + + test_equals_differentPaths() { + expect( + file == fileSystem.entityForPath(join(tempPath, 'file2.txt')), isFalse); + } + + test_equals_samePath() { + expect( + file == fileSystem.entityForPath(join(tempPath, 'file.txt')), isTrue); + } + + test_hashCode_samePath() { + expect(file.hashCode, + fileSystem.entityForPath(join(tempPath, 'file.txt')).hashCode); + } + + test_path() { + expect(file.path, path); + } + + test_readAsBytes_badUtf8() async { + // A file containing invalid UTF-8 can still be read as raw bytes. + List bytes = [0xc0, 0x40]; // Invalid UTF-8 + file.writeAsBytesSync(bytes); + expect(await file.readAsBytes(), bytes); + } + + test_readAsBytes_doesNotExist() { + expect(file.readAsBytes(), throwsException); + } + + test_readAsBytes_exists() async { + var s = 'contents'; + file.writeAsStringSync(s); + expect(await file.readAsBytes(), UTF8.encode(s)); + } + + test_readAsString_badUtf8() { + file.writeAsBytesSync([0xc0, 0x40]); // Invalid UTF-8 + expect(file.readAsString(), throwsException); + } + + test_readAsString_doesNotExist() { + expect(file.readAsString(), throwsException); + } + + test_readAsString_exists() async { + var s = 'contents'; + file.writeAsStringSync(s); + expect(await file.readAsString(), s); + } + + test_readAsString_utf8() async { + file.writeAsBytesSync([0xe2, 0x82, 0xac]); // Unicode € symbol, in UTF-8 + expect(await file.readAsString(), '\u20ac'); + } + + test_writeAsBytesSync_modifyAfterRead() async { + file.writeAsBytesSync([1]); + (await file.readAsBytes())[0] = 2; + expect(await file.readAsBytes(), [1]); + } + + test_writeAsBytesSync_modifyAfterWrite() async { + var bytes = [1]; + file.writeAsBytesSync(bytes); + bytes[0] = 2; + expect(await file.readAsBytes(), [1]); + } + + test_writeAsBytesSync_overwrite() async { + file.writeAsBytesSync([1]); + file.writeAsBytesSync([2]); + expect(await file.readAsBytes(), [2]); + } + + test_writeAsStringSync_overwrite() async { + file.writeAsStringSync('first'); + file.writeAsStringSync('second'); + expect(await file.readAsString(), 'second'); + } + + test_writeAsStringSync_utf8() async { + file.writeAsStringSync('\u20ac'); // Unicode € symbol + expect(await file.readAsBytes(), [0xe2, 0x82, 0xac]); + } +} + +abstract class MemoryFileSystemTestMixin extends _BaseTest { + Uri tempUri; + + setUp() { + super.setUp(); + tempUri = fileSystem.context.toUri(tempPath); + } + + test_entityForPath() { + var path = join(tempPath, 'file.txt'); + expect(fileSystem.entityForPath(path).path, path); + } + + test_entityForPath_absolutize() { + expect(fileSystem.entityForPath('file.txt').path, + join(fileSystem.currentDirectory, 'file.txt')); + } + + test_entityForPath_normalize_dot() { + expect(fileSystem.entityForPath(join(tempPath, '.', 'file.txt')).path, + join(tempPath, 'file.txt')); + } + + test_entityForPath_normalize_dotDot() { + expect( + fileSystem.entityForPath(join(tempPath, 'foo', '..', 'file.txt')).path, + join(tempPath, 'file.txt')); + } + + test_entityForUri() { + expect(fileSystem.entityForUri(Uri.parse('$tempUri/file.txt')).path, + join(tempPath, 'file.txt')); + } + + test_entityForUri_bareUri_absolute() { + expect(() => fileSystem.entityForUri(Uri.parse('/file.txt')), + throwsA(new isInstanceOf())); + } + + test_entityForUri_bareUri_relative() { + expect(() => fileSystem.entityForUri(Uri.parse('file.txt')), + throwsA(new isInstanceOf())); + } + + test_entityForUri_fileUri_relative() { + // A weird quirk of the Uri class is that it doesn't seem possible to create + // a `file:` uri with a relative path, no matter how many slashes you use or + // if you populate the fields directly. But just to be certain, try to do + // so, and make that `file:` uris with relative paths are rejected. + for (var uri in [ + new Uri(scheme: 'file', path: 'file.txt'), + Uri.parse('file:file.txt'), + Uri.parse('file:/file.txt'), + Uri.parse('file://file.txt'), + Uri.parse('file:///file.txt') + ]) { + if (!uri.path.startsWith('/')) { + expect(() => fileSystem.entityForUri(uri), + throwsA(new isInstanceOf())); + } + } + } + + test_entityForUri_nonFileUri() { + expect(() => fileSystem.entityForUri(Uri.parse('package:foo/bar.dart')), + throwsA(new isInstanceOf())); + } + + test_entityForUri_normalize_dot() { + expect(fileSystem.entityForUri(Uri.parse('$tempUri/./file.txt')).path, + join(tempPath, 'file.txt')); + } + + test_entityForUri_normalize_dotDot() { + expect(fileSystem.entityForUri(Uri.parse('$tempUri/foo/../file.txt')).path, + join(tempPath, 'file.txt')); + } +} + +@reflectiveTest +class MemoryFileSystemTestNative extends _BaseTestNative + with MemoryFileSystemTestMixin {} + +@reflectiveTest +class MemoryFileSystemTestPosix extends _BaseTestPosix + with MemoryFileSystemTestMixin {} + +@reflectiveTest +class MemoryFileSystemTestWindows extends _BaseTestWindows + with MemoryFileSystemTestMixin {} + +abstract class _BaseTest { + MemoryFileSystem get fileSystem; + String get tempPath; + String join(String path1, String path2, [String path3, String path4]); + void setUp(); +} + +class _BaseTestNative extends _BaseTest { + MemoryFileSystem fileSystem; + String tempPath; + + String join(String path1, String path2, [String path3, String path4]) => + pathos.join(path1, path2, path3, path4); + + setUp() { + tempPath = pathos.join(io.Directory.systemTemp.path, 'test_file_system'); + fileSystem = + new MemoryFileSystem(pathos.context, io.Directory.current.path); + } +} + +class _BaseTestPosix extends _BaseTest { + MemoryFileSystem fileSystem; + String tempPath; + + String join(String path1, String path2, [String path3, String path4]) => + pathos.posix.join(path1, path2, path3, path4); + + void setUp() { + tempPath = '/test_file_system'; + fileSystem = new MemoryFileSystem(pathos.posix, '/cwd'); + } +} + +class _BaseTestWindows extends _BaseTest { + MemoryFileSystem fileSystem; + String tempPath; + + String join(String path1, String path2, [String path3, String path4]) => + pathos.windows.join(path1, path2, path3, path4); + + void setUp() { + tempPath = r'c:\test_file_system'; + fileSystem = new MemoryFileSystem(pathos.windows, r'c:\cwd'); + } +} diff --git a/pkg/front_end/test/physical_file_system_test.dart b/pkg/front_end/test/physical_file_system_test.dart new file mode 100644 index 000000000000..5c07a8c7c6a9 --- /dev/null +++ b/pkg/front_end/test/physical_file_system_test.dart @@ -0,0 +1,212 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// SharedOptions=--supermixin + +library front_end.test.physical_file_system_test; + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:front_end/file_system.dart'; +import 'package:front_end/physical_file_system.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PhysicalFileSystemTest); + defineReflectiveTests(FileTest); + }); +} + +@reflectiveTest +class FileTest extends _BaseTest { + String path; + FileSystemEntity file; + + setUp() { + super.setUp(); + path = p.join(tempPath, 'file.txt'); + file = PhysicalFileSystem.instance.entityForPath(path); + } + + test_equals_differentPaths() { + expect( + file == + PhysicalFileSystem.instance + .entityForPath(p.join(tempPath, 'file2.txt')), + isFalse); + } + + test_equals_samePath() { + expect( + file == + PhysicalFileSystem.instance + .entityForPath(p.join(tempPath, 'file.txt')), + isTrue); + } + + test_hashCode_samePath() { + expect( + file.hashCode, + PhysicalFileSystem.instance + .entityForPath(p.join(tempPath, 'file.txt')) + .hashCode); + } + + test_path() { + expect(file.path, path); + } + + test_readAsBytes_badUtf8() async { + // A file containing invalid UTF-8 can still be read as raw bytes. + List bytes = [0xc0, 0x40]; // Invalid UTF-8 + new io.File(path).writeAsBytesSync(bytes); + expect(await file.readAsBytes(), bytes); + } + + test_readAsBytes_doesNotExist() { + expect(file.readAsBytes(), throwsException); + } + + test_readAsBytes_exists() async { + var s = 'contents'; + new io.File(path).writeAsStringSync(s); + expect(await file.readAsBytes(), UTF8.encode(s)); + } + + test_readAsString_badUtf8() { + new io.File(path).writeAsBytesSync([0xc0, 0x40]); // Invalid UTF-8 + expect(file.readAsString(), throwsException); + } + + test_readAsString_doesNotExist() { + expect(file.readAsString(), throwsException); + } + + test_readAsString_exists() async { + var s = 'contents'; + new io.File(path).writeAsStringSync(s); + expect(await file.readAsString(), s); + } + + test_readAsString_utf8() async { + var bytes = [0xe2, 0x82, 0xac]; // Unicode € symbol (in UTF-8) + new io.File(path).writeAsBytesSync(bytes); + expect(await file.readAsString(), '\u20ac'); + } +} + +@reflectiveTest +class PhysicalFileSystemTest extends _BaseTest { + Uri tempUri; + + setUp() { + super.setUp(); + tempUri = new Uri.directory(tempPath); + } + + test_entityForPath() { + var path = p.join(tempPath, 'file.txt'); + expect(PhysicalFileSystem.instance.entityForPath(path).path, path); + } + + test_entityForPath_absolutize() { + expect(PhysicalFileSystem.instance.entityForPath('file.txt').path, + new io.File('file.txt').absolute.path); + } + + test_entityForPath_normalize_dot() { + expect( + PhysicalFileSystem.instance + .entityForPath(p.join(tempPath, '.', 'file.txt')) + .path, + p.join(tempPath, 'file.txt')); + } + + test_entityForPath_normalize_dotDot() { + expect( + PhysicalFileSystem.instance + .entityForPath(p.join(tempPath, 'foo', '..', 'file.txt')) + .path, + p.join(tempPath, 'file.txt')); + } + + test_entityForUri() { + expect( + PhysicalFileSystem.instance + .entityForUri(Uri.parse('$tempUri/file.txt')) + .path, + p.join(tempPath, 'file.txt')); + } + + test_entityForUri_bareUri_absolute() { + expect( + () => PhysicalFileSystem.instance.entityForUri(Uri.parse('/file.txt')), + throwsA(new isInstanceOf())); + } + + test_entityForUri_bareUri_relative() { + expect( + () => PhysicalFileSystem.instance.entityForUri(Uri.parse('file.txt')), + throwsA(new isInstanceOf())); + } + + test_entityForUri_fileUri_relative() { + // A weird quirk of the Uri class is that it doesn't seem possible to create + // a `file:` uri with a relative path, no matter how many slashes you use or + // if you populate the fields directly. But just to be certain, try to do + // so, and make that `file:` uris with relative paths are rejected. + for (var uri in [ + new Uri(scheme: 'file', path: 'file.txt'), + Uri.parse('file:file.txt'), + Uri.parse('file:/file.txt'), + Uri.parse('file://file.txt'), + Uri.parse('file:///file.txt') + ]) { + if (!uri.path.startsWith('/')) { + expect(() => PhysicalFileSystem.instance.entityForUri(uri), + throwsA(new isInstanceOf())); + } + } + } + + test_entityForUri_nonFileUri() { + expect( + () => PhysicalFileSystem.instance + .entityForUri(Uri.parse('package:foo/bar.dart')), + throwsA(new isInstanceOf())); + } + + test_entityForUri_normalize_dot() { + expect( + PhysicalFileSystem.instance + .entityForUri(Uri.parse('$tempUri/./file.txt')) + .path, + p.join(tempPath, 'file.txt')); + } + + test_entityForUri_normalize_dotDot() { + expect( + PhysicalFileSystem.instance + .entityForUri(Uri.parse('$tempUri/foo/../file.txt')) + .path, + p.join(tempPath, 'file.txt')); + } +} + +class _BaseTest { + io.Directory tempDirectory; + String tempPath; + + setUp() { + tempDirectory = io.Directory.systemTemp.createTempSync('test_file_system'); + tempPath = tempDirectory.absolute.path; + } + + tearDown() { + tempDirectory.deleteSync(recursive: true); + } +} diff --git a/pkg/pkg.status b/pkg/pkg.status index d0913e69f4ab..de275de6e453 100644 --- a/pkg/pkg.status +++ b/pkg/pkg.status @@ -53,6 +53,8 @@ compiler/tool/*: SkipByDesign # Only meant to run on vm front_end/tool/*: SkipByDesign # Only meant to run on vm lookup_map/test/version_check_test: SkipByDesign # Only meant to run in vm. typed_data/test/typed_buffers_test/01: Fail # Not supporting Int64List, Uint64List. +front_end/test/memory_file_system_test: CompileTimeError # Issue 23773 +front_end/test/physical_file_system_test: SkipByDesign # Uses dart:io [ $compiler == dart2js && $builder_tag != dart2js_analyzer ] analyzer/test/*: Skip # Issue 26813