@@ -24,6 +24,7 @@ class TestFileManager : XCTestCase {
2424 ( " test_contentsOfDirectoryAtPath " , test_contentsOfDirectoryAtPath) ,
2525 ( " test_subpathsOfDirectoryAtPath " , test_subpathsOfDirectoryAtPath) ,
2626 ( " test_copyItemAtPathToPath " , test_copyItemAtPathToPath) ,
27+ ( " test_linkItemAtPathToPath " , test_linkItemAtPathToPath) ,
2728 ( " test_homedirectoryForUser " , test_homedirectoryForUser) ,
2829 ( " test_temporaryDirectoryForUser " , test_temporaryDirectoryForUser) ,
2930 ( " test_creatingDirectoryWithShortIntermediatePath " , test_creatingDirectoryWithShortIntermediatePath) ,
@@ -550,7 +551,13 @@ class TestFileManager : XCTestCase {
550551 XCTFail ( " Failed to clean up files " )
551552 }
552553 }
553-
554+
555+ private func directoryExists( atPath path: String ) -> Bool {
556+ var isDir : ObjCBool = false
557+ let exists = FileManager . default. fileExists ( atPath: path, isDirectory: & isDir)
558+ return exists && isDir. boolValue
559+ }
560+
554561 func test_copyItemAtPathToPath( ) {
555562 let fm = FileManager . default
556563 let srcPath = NSTemporaryDirectory ( ) + " testdir \( NSUUID ( ) . uuidString) "
@@ -560,13 +567,7 @@ class TestFileManager : XCTestCase {
560567 ignoreError { try fm. removeItem ( atPath: srcPath) }
561568 ignoreError { try fm. removeItem ( atPath: destPath) }
562569 }
563-
564- func directoryExists( atPath path: String ) -> Bool {
565- var isDir : ObjCBool = false
566- let exists = fm. fileExists ( atPath: path, isDirectory: & isDir)
567- return exists && isDir. boolValue
568- }
569-
570+
570571 func createDirectory( atPath path: String ) {
571572 do {
572573 try fm. createDirectory ( atPath: path, withIntermediateDirectories: false , attributes: nil )
@@ -638,7 +639,85 @@ class TestFileManager : XCTestCase {
638639 // ignore
639640 }
640641 }
641-
642+
643+ func test_linkItemAtPathToPath( ) {
644+ let fm = FileManager . default
645+ let basePath = NSTemporaryDirectory ( ) + " linkItemAtPathToPath/ "
646+ let srcPath = basePath + " testdir \( NSUUID ( ) . uuidString) "
647+ let destPath = basePath + " testdir \( NSUUID ( ) . uuidString) "
648+ defer { ignoreError { try fm. removeItem ( atPath: basePath) } }
649+
650+ func getFileInfo( atPath path: String , _ body: ( String , Bool , UInt64 , UInt64 ) -> ( ) ) {
651+ guard let enumerator = fm. enumerator ( atPath: path) else {
652+ XCTFail ( " Cant enumerate \( path) " )
653+ return
654+ }
655+ while let item = enumerator. nextObject ( ) as? String {
656+ let fname = " \( path) / \( item) "
657+ do {
658+ let attrs = try fm. attributesOfItem ( atPath: fname)
659+ let inode = ( attrs [ . systemFileNumber] as? NSNumber ) ? . uint64Value
660+ let linkCount = ( attrs [ . referenceCount] as? NSNumber ) ? . uint64Value
661+ let ftype = attrs [ . type] as? FileAttributeType
662+
663+ if inode == nil || linkCount == nil || ftype == nil {
664+ XCTFail ( " Unable to get attributes of \( fname) " )
665+ return
666+ }
667+ let isDir = ( ftype == . typeDirectory)
668+ body ( item, isDir, inode!, linkCount!)
669+ } catch {
670+ XCTFail ( " Unable to get attributes of \( fname) : \( error) " )
671+ return
672+ }
673+ }
674+ }
675+
676+ ignoreError { try fm. removeItem ( atPath: basePath) }
677+ XCTAssertNotNil ( try ? fm. createDirectory ( atPath: " \( srcPath) /tempdir/subdir/otherdir/extradir " , withIntermediateDirectories: true , attributes: nil ) )
678+ XCTAssertTrue ( fm. createFile ( atPath: " \( srcPath) /tempdir/tempfile " , contents: Data ( ) , attributes: nil ) )
679+ XCTAssertTrue ( fm. createFile ( atPath: " \( srcPath) /tempdir/tempfile2 " , contents: Data ( ) , attributes: nil ) )
680+ XCTAssertTrue ( fm. createFile ( atPath: " \( srcPath) /tempdir/subdir/otherdir/extradir/tempfile2 " , contents: Data ( ) , attributes: nil ) )
681+
682+ var fileInfos : [ String : ( Bool , UInt64 , UInt64 ) ] = [ : ]
683+ getFileInfo ( atPath: srcPath, { name, isDir, inode, linkCount in
684+ fileInfos [ name] = ( isDir, inode, linkCount)
685+ } )
686+ XCTAssertEqual ( fileInfos. count, 7 )
687+ XCTAssertNotNil ( try ? fm. linkItem ( atPath: srcPath, toPath: destPath) , " Unable to link directory " )
688+
689+ getFileInfo ( atPath: destPath, { name, isDir, inode, linkCount in
690+ guard let srcFileInfo = fileInfos. removeValue ( forKey: name) else {
691+ XCTFail ( " Cant find \( name) in \( destPath) " )
692+ return
693+ }
694+ let ( srcIsDir, srcInode, srcLinkCount) = srcFileInfo
695+ XCTAssertEqual ( srcIsDir, isDir, " Directory/File type mismatch " )
696+ if isDir {
697+ XCTAssertEqual ( srcLinkCount, linkCount)
698+ } else {
699+ XCTAssertEqual ( srcInode, inode)
700+ XCTAssertEqual ( srcLinkCount + 1 , linkCount)
701+ }
702+ } )
703+
704+ XCTAssertEqual ( fileInfos. count, 0 )
705+ // linkItem should fail a 2nd time
706+ XCTAssertNil ( try ? fm. linkItem ( atPath: srcPath, toPath: destPath) , " Copy overwrites a file/folder that already exists " )
707+
708+ // Test 'linking' a symlink, which actually does a copy
709+ let srcLink = srcPath + " /testlink "
710+ let destLink = destPath + " /testlink "
711+ do {
712+ try fm. createSymbolicLink ( atPath: srcLink, withDestinationPath: " linkdest " )
713+ try fm. linkItem ( atPath: srcLink, toPath: destLink)
714+ XCTAssertEqual ( try fm. destinationOfSymbolicLink ( atPath: destLink) , " linkdest " )
715+ } catch {
716+ XCTFail ( " \( error) " )
717+ }
718+ XCTAssertNil ( try ? fm. linkItem ( atPath: srcLink, toPath: destLink) , " Creating link where one already exists " )
719+ }
720+
642721 func test_homedirectoryForUser( ) {
643722 let filemanger = FileManager . default
644723 XCTAssertNil ( filemanger. homeDirectory ( forUser: " someuser " ) )
0 commit comments