Skip to content

Commit 7fa1973

Browse files
authored
Merge pull request #163 from apple/make-inmemoryfilesystem-threadsafe
Makes InMemoryFileSystem threadsafe to address flaky unit tests
2 parents 99ae28c + 4fd3bac commit 7fa1973

File tree

1 file changed

+162
-108
lines changed

1 file changed

+162
-108
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 162 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,9 @@ private class LocalFileSystem: FileSystem {
488488
//
489489
/// Concrete FileSystem implementation which simulates an empty disk.
490490
public class InMemoryFileSystem: FileSystem {
491+
492+
/// Private internal representation of a file system node.
493+
/// Not threadsafe.
491494
private class Node {
492495
/// The actual node data.
493496
let contents: NodeContents
@@ -501,6 +504,9 @@ public class InMemoryFileSystem: FileSystem {
501504
return Node(contents.copy())
502505
}
503506
}
507+
508+
/// Private internal representation the contents of a file system node.
509+
/// Not threadsafe.
504510
private enum NodeContents {
505511
case file(ByteString)
506512
case directory(DirectoryContents)
@@ -518,6 +524,9 @@ public class InMemoryFileSystem: FileSystem {
518524
}
519525
}
520526
}
527+
528+
/// Private internal representation the contents of a directory.
529+
/// Not threadsafe.
521530
private class DirectoryContents {
522531
var entries: [String: Node]
523532

@@ -535,9 +544,16 @@ public class InMemoryFileSystem: FileSystem {
535544
}
536545
}
537546

538-
/// The root filesytem.
547+
/// The root node of the filesytem.
539548
private var root: Node
540-
/// Used to access lockFiles in a thread safe manner.
549+
550+
/// Protects `root` and everything underneath it.
551+
/// FIXME: Using a single lock for this is a performance problem, but in
552+
/// reality, the only practical use for InMemoryFileSystem is for unit
553+
/// tests.
554+
private let lock = Lock()
555+
556+
/// Exclusive file system lock vended to clients through `withLock()`.
541557
private let lockFilesLock = Lock()
542558

543559
public init() {
@@ -546,12 +562,15 @@ public class InMemoryFileSystem: FileSystem {
546562

547563
/// Creates deep copy of the object.
548564
public func copy() -> InMemoryFileSystem {
549-
let fs = InMemoryFileSystem()
550-
fs.root = root.copy()
551-
return fs
565+
return lock.withLock {
566+
let fs = InMemoryFileSystem()
567+
fs.root = root.copy()
568+
return fs
569+
}
552570
}
553571

554-
/// Get the node corresponding to the given path.
572+
/// Private function to look up the node corresponding to a path.
573+
/// Not threadsafe.
555574
private func getNode(_ path: AbsolutePath, followSymlink: Bool = true) throws -> Node? {
556575
func getNodeInternal(_ path: AbsolutePath) throws -> Node? {
557576
// If this is the root node, return it.
@@ -590,46 +609,54 @@ public class InMemoryFileSystem: FileSystem {
590609
// MARK: FileSystem Implementation
591610

592611
public func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool {
593-
do {
594-
switch try getNode(path, followSymlink: followSymlink)?.contents {
595-
case .file, .directory, .symlink: return true
596-
case .none: return false
612+
return lock.withLock {
613+
do {
614+
switch try getNode(path, followSymlink: followSymlink)?.contents {
615+
case .file, .directory, .symlink: return true
616+
case .none: return false
617+
}
618+
} catch {
619+
return false
597620
}
598-
} catch {
599-
return false
600621
}
601622
}
602623

603624
public func isDirectory(_ path: AbsolutePath) -> Bool {
604-
do {
605-
if case .directory? = try getNode(path)?.contents {
606-
return true
625+
return lock.withLock {
626+
do {
627+
if case .directory? = try getNode(path)?.contents {
628+
return true
629+
}
630+
return false
631+
} catch {
632+
return false
607633
}
608-
return false
609-
} catch {
610-
return false
611634
}
612635
}
613636

614637
public func isFile(_ path: AbsolutePath) -> Bool {
615-
do {
616-
if case .file? = try getNode(path)?.contents {
617-
return true
638+
return lock.withLock {
639+
do {
640+
if case .file? = try getNode(path)?.contents {
641+
return true
642+
}
643+
return false
644+
} catch {
645+
return false
618646
}
619-
return false
620-
} catch {
621-
return false
622647
}
623648
}
624649

625650
public func isSymlink(_ path: AbsolutePath) -> Bool {
626-
do {
627-
if case .symlink? = try getNode(path, followSymlink: false)?.contents {
628-
return true
651+
return lock.withLock {
652+
do {
653+
if case .symlink? = try getNode(path, followSymlink: false)?.contents {
654+
return true
655+
}
656+
return false
657+
} catch {
658+
return false
629659
}
630-
return false
631-
} catch {
632-
return false
633660
}
634661
}
635662

@@ -658,18 +685,21 @@ public class InMemoryFileSystem: FileSystem {
658685
}
659686

660687
public func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
661-
guard let node = try getNode(path) else {
662-
throw FileSystemError.noEntry
663-
}
664-
guard case .directory(let contents) = node.contents else {
665-
throw FileSystemError.notDirectory
666-
}
688+
return try lock.withLock {
689+
guard let node = try getNode(path) else {
690+
throw FileSystemError.noEntry
691+
}
692+
guard case .directory(let contents) = node.contents else {
693+
throw FileSystemError.notDirectory
694+
}
667695

668-
// FIXME: Perhaps we should change the protocol to allow lazy behavior.
669-
return [String](contents.entries.keys)
696+
// FIXME: Perhaps we should change the protocol to allow lazy behavior.
697+
return [String](contents.entries.keys)
698+
}
670699
}
671-
672-
public func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
700+
701+
/// Not threadsafe.
702+
private func _createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
673703
// Ignore if client passes root.
674704
guard !path.isRoot else {
675705
return
@@ -681,10 +711,10 @@ public class InMemoryFileSystem: FileSystem {
681711
// to create the parent and retry.
682712
if recursive && path != parentPath {
683713
// Attempt to create the parent.
684-
try createDirectory(parentPath, recursive: true)
714+
try _createDirectory(parentPath, recursive: true)
685715

686716
// Re-attempt creation, non-recursively.
687-
return try createDirectory(path, recursive: false)
717+
return try _createDirectory(path, recursive: false)
688718
} else {
689719
// Otherwise, we failed.
690720
throw FileSystemError.noEntry
@@ -713,71 +743,83 @@ public class InMemoryFileSystem: FileSystem {
713743
contents.entries[path.basename] = Node(.directory(DirectoryContents()))
714744
}
715745

716-
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
717-
// Create directory to destination parent.
718-
guard let destinationParent = try getNode(path.parentDirectory) else {
719-
throw FileSystemError.noEntry
746+
public func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
747+
return try lock.withLock {
748+
try _createDirectory(path, recursive: recursive)
720749
}
750+
}
721751

722-
// Check that the parent is a directory.
723-
guard case .directory(let contents) = destinationParent.contents else {
724-
throw FileSystemError.notDirectory
725-
}
752+
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
753+
return try lock.withLock {
754+
// Create directory to destination parent.
755+
guard let destinationParent = try getNode(path.parentDirectory) else {
756+
throw FileSystemError.noEntry
757+
}
726758

727-
guard contents.entries[path.basename] == nil else {
728-
throw FileSystemError.alreadyExistsAtDestination
729-
}
759+
// Check that the parent is a directory.
760+
guard case .directory(let contents) = destinationParent.contents else {
761+
throw FileSystemError.notDirectory
762+
}
763+
764+
guard contents.entries[path.basename] == nil else {
765+
throw FileSystemError.alreadyExistsAtDestination
766+
}
730767

731-
let destination = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString
768+
let destination = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString
732769

733-
contents.entries[path.basename] = Node(.symlink(destination))
770+
contents.entries[path.basename] = Node(.symlink(destination))
771+
}
734772
}
735773

736774
public func readFileContents(_ path: AbsolutePath) throws -> ByteString {
737-
// Get the node.
738-
guard let node = try getNode(path) else {
739-
throw FileSystemError.noEntry
740-
}
775+
return try lock.withLock {
776+
// Get the node.
777+
guard let node = try getNode(path) else {
778+
throw FileSystemError.noEntry
779+
}
741780

742-
// Check that the node is a file.
743-
guard case .file(let contents) = node.contents else {
744-
// The path is a directory, this is an error.
745-
throw FileSystemError.isDirectory
746-
}
781+
// Check that the node is a file.
782+
guard case .file(let contents) = node.contents else {
783+
// The path is a directory, this is an error.
784+
throw FileSystemError.isDirectory
785+
}
747786

748-
// Return the file contents.
749-
return contents
787+
// Return the file contents.
788+
return contents
789+
}
750790
}
751791

752792
public func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws {
753-
// It is an error if this is the root node.
754-
let parentPath = path.parentDirectory
755-
guard path != parentPath else {
756-
throw FileSystemError.isDirectory
757-
}
793+
return try lock.withLock {
794+
// It is an error if this is the root node.
795+
let parentPath = path.parentDirectory
796+
guard path != parentPath else {
797+
throw FileSystemError.isDirectory
798+
}
758799

759-
// Get the parent node.
760-
guard let parent = try getNode(parentPath) else {
761-
throw FileSystemError.noEntry
762-
}
800+
// Get the parent node.
801+
guard let parent = try getNode(parentPath) else {
802+
throw FileSystemError.noEntry
803+
}
763804

764-
// Check that the parent is a directory.
765-
guard case .directory(let contents) = parent.contents else {
766-
// The parent isn't a directory, this is an error.
767-
throw FileSystemError.notDirectory
768-
}
805+
// Check that the parent is a directory.
806+
guard case .directory(let contents) = parent.contents else {
807+
// The parent isn't a directory, this is an error.
808+
throw FileSystemError.notDirectory
809+
}
769810

770-
// Check if the node exists.
771-
if let node = contents.entries[path.basename] {
772-
// Verify it is a file.
773-
guard case .file = node.contents else {
774-
// The path is a directory, this is an error.
775-
throw FileSystemError.isDirectory
811+
// Check if the node exists.
812+
if let node = contents.entries[path.basename] {
813+
// Verify it is a file.
814+
guard case .file = node.contents else {
815+
// The path is a directory, this is an error.
816+
throw FileSystemError.isDirectory
817+
}
776818
}
777-
}
778819

779-
// Write the file.
780-
contents.entries[path.basename] = Node(.file(bytes))
820+
// Write the file.
821+
contents.entries[path.basename] = Node(.file(bytes))
822+
}
781823
}
782824

783825
public func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
@@ -787,21 +829,25 @@ public class InMemoryFileSystem: FileSystem {
787829
}
788830

789831
public func removeFileTree(_ path: AbsolutePath) throws {
790-
// Ignore root and get the parent node's content if its a directory.
791-
guard !path.isRoot,
792-
let parent = try? getNode(path.parentDirectory),
793-
case .directory(let contents) = parent.contents else {
794-
return
832+
return lock.withLock {
833+
// Ignore root and get the parent node's content if its a directory.
834+
guard !path.isRoot,
835+
let parent = try? getNode(path.parentDirectory),
836+
case .directory(let contents) = parent.contents else {
837+
return
838+
}
839+
// Set it to nil to release the contents.
840+
contents.entries[path.basename] = nil
795841
}
796-
// Set it to nil to release the contents.
797-
contents.entries[path.basename] = nil
798842
}
799843

800844
public func chmod(_ mode: FileMode, path: AbsolutePath, options: Set<FileMode.Option>) throws {
801845
// FIXME: We don't have these semantics in InMemoryFileSystem.
802846
}
803-
804-
public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
847+
848+
/// Private implementation of core copying function.
849+
/// Not threadsafe.
850+
private func _copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
805851
// Get the source node.
806852
guard let source = try getNode(sourcePath) else {
807853
throw FileSystemError.noEntry
@@ -824,20 +870,28 @@ public class InMemoryFileSystem: FileSystem {
824870
contents.entries[destinationPath.basename] = source
825871
}
826872

827-
public func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
828-
// Get the source parent node.
829-
guard let sourceParent = try getNode(sourcePath.parentDirectory) else {
830-
throw FileSystemError.noEntry
873+
public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
874+
return try lock.withLock {
875+
try _copy(from: sourcePath, to: destinationPath)
831876
}
877+
}
832878

833-
// Check that the parent is a directory.
834-
guard case .directory(let contents) = sourceParent.contents else {
835-
throw FileSystemError.notDirectory
836-
}
879+
public func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
880+
return try lock.withLock {
881+
// Get the source parent node.
882+
guard let sourceParent = try getNode(sourcePath.parentDirectory) else {
883+
throw FileSystemError.noEntry
884+
}
885+
886+
// Check that the parent is a directory.
887+
guard case .directory(let contents) = sourceParent.contents else {
888+
throw FileSystemError.notDirectory
889+
}
837890

838-
try copy(from: sourcePath, to: destinationPath)
891+
try _copy(from: sourcePath, to: destinationPath)
839892

840-
contents.entries[sourcePath.basename] = nil
893+
contents.entries[sourcePath.basename] = nil
894+
}
841895
}
842896

843897
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {

0 commit comments

Comments
 (0)