1# coding=utf-8 2# 3# test_exfat_userfs.py -- Test cases for the ExFAT plug-in for UserFS. 4# 5# Copyright (c) 2013 Apple Inc. All rights reserved. 6# 7import sys 8import os 9import io 10import subprocess 11import shutil 12import struct 13import difflib 14import random 15import hashlib 16import unittest 17import msdosfs 18from msdosfs import ATTR_READ_ONLY, ATTR_HIDDEN, ATTR_SYSTEM, ATTR_ARCHIVE, make_long_dirent, Timestamp 19 20class UserFSTestCase(unittest.TestCase): 21 imagePath = '/tmp/FAT_TestCase.sparsebundle' 22 imageSize = '1g' 23 device = None 24 25 @classmethod 26 def runProcess(klass, args, expectedReturnCode=None, expectedStdout=None, expectedStderr=None, env=None): 27 if VERBOSE: 28 print "->", " ".join(args) 29 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) 30 stdout, stderr = p.communicate() 31 returncode = p.returncode 32 if VERBOSE: 33 sys.stdout.write(stdout) 34 sys.stdout.write(stderr) 35 if returncode: 36 print "## Process exited with return code:", returncode 37 if expectedStderr is not None: 38 if stderr != expectedStderr: 39 diffs = '\n'.join(difflib.unified_diff(expectedStderr.splitlines(), stderr.splitlines(), 'Expected', 'Actual', lineterm='')) 40 assert stderr == expectedStderr, 'Standard error mismatch:\n{0}'.format(diffs) 41 if expectedStdout is not None: 42 if stdout != expectedStdout: 43 diffs = '\n'.join(difflib.unified_diff(expectedStdout.splitlines(), stdout.splitlines(), 'Expected', 'Actual', lineterm='')) 44 assert stdout == expectedStdout, 'Standard output mismatch:\n{0}'.format(diffs) 45 if expectedReturnCode is not None: 46 assert returncode == expectedReturnCode, "Exit status was {0} (expected {1})".format(returncode, expectedReturnCode) 47 return returncode, stdout, stderr 48 49 @classmethod 50 def createDiskImage(klass, size=None): 51 'Create the disk image, if it does not exist yet' 52 if not os.path.exists(klass.imagePath): 53 args = "/usr/bin/hdiutil create -size <SIZE> -layout MBRSPUD -partitionType DOS_FAT_32".split() 54 args[3] = size if size else klass.imageSize 55 args.append(klass.imagePath) 56 klass.runProcess(args, 0, None, "") 57 58 @classmethod 59 def attachDiskImage(klass, readOnly=False): 60 if not klass.device: 61 args='hdiutil attach -nomount'.split() 62 if readOnly: 63 args.append('-readonly') 64 args.append(klass.imagePath) 65 try: 66 returncode, stdout, stderr = klass.runProcess(args, 0, None, "") 67 stdout = stdout.splitlines() 68 assert(len(stdout) == 2) 69 klass.wholeDisk = stdout[0].split()[0] 70 klass.partition = stdout[1].split()[0] 71 assert(klass.partition.startswith("/dev/")) 72 klass.device = klass.partition[5:] 73 except: 74 if os.path.isdir(klass.imagePath): 75 shutil.rmtree(klass.imagePath) 76 else: 77 os.remove(klass.imagePath) 78 raise 79 80 @classmethod 81 def ejectDiskImage(klass, keep=None): 82 if keep is None: 83 keep = KEEP_IMAGE 84 if keep: 85 print '#', klass.device, 'is still attached. To clean up:' 86 print 'diskutil eject', klass.wholeDisk 87 else: 88 args = 'diskutil eject'.split() + [klass.wholeDisk] 89 klass.runProcess(args, 0, None, '') 90 klass.device = None 91 92 @classmethod 93 def deleteDiskImage(klass): 94 if KEEP_IMAGE: 95 print "rm -r", klass.imagePath 96 else: 97 if VERBOSE: 98 print "-> Deleting %s" % klass.imagePath 99 if os.path.isdir(klass.imagePath): 100 shutil.rmtree(klass.imagePath) 101 else: 102 os.remove(klass.imagePath) 103 104 def runTool(self, args, expectedReturnCode=None, expectedStdout=None, expectedStderr=None): 105 # Add the default tool path and device path to the args 106 args = [TOOL_PATH, "--device", self.device] + args 107 108 return self.runProcess(args, expectedReturnCode, expectedStdout, expectedStderr, TOOL_ENV) 109 110 # Formatter=exfat.Formatter 111 112 # Use newfs_msdos to create an empty volume 113 @classmethod 114 def formatPartition(klass, dev_path): 115 args = ['/sbin/newfs_msdos'] 116 if hasattr(klass, 'volumeName'): 117 args.extend(['-v', klass.volumeName]) 118 args.append(dev_path) 119 klass.runProcess(args, 0) 120 121 @classmethod 122 def setUpClass(klass): 123 if klass.imagePath: 124 klass.createDiskImage() 125 klass.attachDiskImage() 126 else: 127 assert klass.device is not None 128 klass.partition = "/dev/"+klass.device 129 130 # Format the device 131 if VERBOSE: 132 print "-> Formatting %s" % klass.partition 133 klass.formatPartition(klass.partition) 134 135 # If there is a prepareContent class method, then "mount" the volume 136 # and call the prepareContent method. 137 if hasattr(klass, 'prepareContent'): 138 with open(klass.partition, "r+") as dev: 139 volume = msdosfs.msdosfs(dev) 140 klass.prepareContent(volume) 141 volume.flush() 142 143 @classmethod 144 def tearDownClass(klass): 145 if klass.imagePath: 146 klass.ejectDiskImage() 147 # Don't bother deleting the disk image 148 149 def setUp(self): 150 if VERBOSE: 151 print # So verbose output starts on line following test name 152 self.runProcess([FSCK_PATH, '-n', self.device], 0) 153 154 def tearDown(self): 155 self.runProcess([FSCK_PATH, '-n', self.device], 0) 156 157 def verifyDirty(self): 158 "Verify the volume is marked dirty." 159 expectedErr = 'QUICKCHECK ONLY; FILESYSTEM DIRTY\n' 160 self.runProcess([FSCK_PATH, '-q', self.device], 3, '', expectedErr) 161 162 def verifyClean(self): 163 "Verify the volume is marked clean." 164 expectedErr = 'QUICKCHECK ONLY; FILESYSTEM CLEAN\n' 165 self.runProcess([FSCK_PATH, '-q', self.device], 0, '', expectedErr) 166 167class TestEmptyVolume(UserFSTestCase): 168 volumeName = 'EMPTY' 169 170 def testInfoRoot(self): 171 args = ["--info", "/"] 172 expected = " 4096 dir Jan 1 00:00:00.00 1980 /\n" 173 self.runTool(args, 0, expected, "") 174 175 def testListRoot(self): 176 args = ["--list", "/"] 177 expected = "" 178 self.runTool(args, 0, expected, "") 179 180 def testInfoMissingFile(self): 181 args = ["--info", "/missing"] 182 expectedErr = "userfs_tool: /missing: The operation couldn’t be completed. No such file or directory\n" 183 self.runTool(args, 1, "", expectedErr) 184 185 def testReadMissingFile(self): 186 args = ["--read", "/missing"] 187 expectedErr = "userfs_tool: /missing: The operation couldn’t be completed. No such file or directory\n" 188 self.runTool(args, 1, "", expectedErr) 189 190 def testSHA1MissingFile(self): 191 args = ["--sha", "/missing"] 192 expectedErr = "userfs_tool: /missing: The operation couldn’t be completed. No such file or directory\n" 193 self.runTool(args, 1, "", expectedErr) 194 195 def testDeleteMissingFile(self): 196 args = ["--delete", "/missing"] 197 expectedErr = "userfs_tool: /missing: The operation couldn’t be completed. No such file or directory\n" 198 self.runTool(args, 1, "", expectedErr) 199 200class TestFileInfo(UserFSTestCase): 201 volumeName = 'FILEINFO' 202 203 @classmethod 204 def prepareContent(klass, volume): 205 def makeFiles(parent): 206 parent.mkfile('UPPER.TXT') # Short name, all upper case 207 parent.mkfile('lower.txt') # Short name, all lower case 208 parent.mkfile('mixed.TXT') # Short name, part lower and part upper 209 parent.mkfile('MyFile.txt') # Long name (mixed case, 1 long name entry) 210 # A long name that ends on a directory entry boundary; no terminator or padding (26 chars) 211 parent.mkfile('abcdefghijklmnopqrstuvwxyz') 212 # A long name with a terminator, but no padding (25 chars) 213 parent.mkfile('The quick brown fox jumps') 214 # A long name of maximum length 215 parent.mkfile('x'*255) 216 # A locked file 217 parent.mkfile('Locked', attributes=ATTR_ARCHIVE|ATTR_READ_ONLY) 218 # A hidden file 219 parent.mkfile('Hidden', attributes=ATTR_ARCHIVE|ATTR_HIDDEN) 220 # A "system" file 221 parent.mkfile('System', attributes=ATTR_ARCHIVE|ATTR_SYSTEM) 222 # A deleted entry 223 parent.mkfile('xDeleted', deleted=True) 224# - In root directory 225# + FAT12 226# + FAT16 227# √ FAT32 228 229 def makeMoreLongNames(parent): 230 slotsPerCluster = volume.bytesPerCluster / 32 231 232 # Make a long name starting at the beginning of a cluster 233 raw = make_long_dirent("Start of Cluster", ATTR_ARCHIVE) 234 parent.fill_slots(slotsPerCluster) 235 parent.write_slots(slotsPerCluster, raw) 236 237 # Make a long name that crosses a cluster boundary 238 raw = make_long_dirent("Crossing a Cluster", ATTR_ARCHIVE) 239 slot = slotsPerCluster * 2 - 1 240 parent.fill_slots(slot) 241 parent.write_slots(slot, raw) 242 243 # Make a long name, where the short name entry is at the end of 244 # the directory cluster (and end of directory) 245 raw = make_long_dirent("End of Cluster", ATTR_ARCHIVE) 246 slot = slotsPerCluster * 3 - (len(raw)/32) 247 parent.fill_slots(slot) 248 parent.write_slots(slot, raw) 249 250 def makeDates(parent): 251 parent.mkfile('Halloween', modDate=Timestamp(10,31,2013,17,0,0)) 252 parent.mkfile("Valentine's", createDate=Timestamp(2,14,2013,22,33,44.551)) 253 parent.mkfile("Loma Prieta", createDate=Timestamp(10,17,1989,17,4,6.9), modDate=Timestamp(10,17,1989,17,4,0)) 254 255 root = volume.root() 256 clusters = volume.fat.find(3) 257 subdir = root.mkdir('SUBDIR', clusters=clusters) 258 dates = root.mkdir('Dates') 259 makeFiles(root) 260 makeFiles(subdir) 261 makeMoreLongNames(subdir) 262 makeDates(dates) 263 264 def testRootItems(self): 265 args = '--list /'.split() 266 expectedOut = """\ 267 12288 dir Jan 1 00:00:00.00 1980 /SUBDIR 268 4096 dir Jan 1 00:00:00.00 1980 /Dates 269 0 file Jan 1 00:00:00.00 1980 /UPPER.TXT 270 0 file Jan 1 00:00:00.00 1980 /lower.txt 271 0 file Jan 1 00:00:00.00 1980 /mixed.TXT 272 0 file Jan 1 00:00:00.00 1980 /MyFile.txt 273 0 file Jan 1 00:00:00.00 1980 /abcdefghijklmnopqrstuvwxyz 274 0 file Jan 1 00:00:00.00 1980 /The quick brown fox jumps 275 0 file Jan 1 00:00:00.00 1980 /xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 276 0 #file Jan 1 00:00:00.00 1980 /Locked 277 0 file Jan 1 00:00:00.00 1980 /Hidden 278 0 file Jan 1 00:00:00.00 1980 /System 279""" 280 self.runTool(args, 0, expectedOut, '') 281 282 def testSubdirItems(self): 283 args = '--list /SUBDIR'.split() 284 expectedOut = """\ 285 0 file Jan 1 00:00:00.00 1980 /SUBDIR/UPPER.TXT 286 0 file Jan 1 00:00:00.00 1980 /SUBDIR/lower.txt 287 0 file Jan 1 00:00:00.00 1980 /SUBDIR/mixed.TXT 288 0 file Jan 1 00:00:00.00 1980 /SUBDIR/MyFile.txt 289 0 file Jan 1 00:00:00.00 1980 /SUBDIR/abcdefghijklmnopqrstuvwxyz 290 0 file Jan 1 00:00:00.00 1980 /SUBDIR/The quick brown fox jumps 291 0 file Jan 1 00:00:00.00 1980 /SUBDIR/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 292 0 #file Jan 1 00:00:00.00 1980 /SUBDIR/Locked 293 0 file Jan 1 00:00:00.00 1980 /SUBDIR/Hidden 294 0 file Jan 1 00:00:00.00 1980 /SUBDIR/System 295 0 file Jan 1 00:00:00.00 1980 /SUBDIR/Start of Cluster 296 0 file Jan 1 00:00:00.00 1980 /SUBDIR/Crossing a Cluster 297 0 file Jan 1 00:00:00.00 1980 /SUBDIR/End of Cluster 298""" 299 self.runTool(args, 0, expectedOut, '') 300 301 def testRecursive(self): 302 args = '--list --recursive /'.split() 303 expectedOut = """\ 304 12288 dir Jan 1 00:00:00.00 1980 /SUBDIR 305 0 file Jan 1 00:00:00.00 1980 /SUBDIR/UPPER.TXT 306 0 file Jan 1 00:00:00.00 1980 /SUBDIR/lower.txt 307 0 file Jan 1 00:00:00.00 1980 /SUBDIR/mixed.TXT 308 0 file Jan 1 00:00:00.00 1980 /SUBDIR/MyFile.txt 309 0 file Jan 1 00:00:00.00 1980 /SUBDIR/abcdefghijklmnopqrstuvwxyz 310 0 file Jan 1 00:00:00.00 1980 /SUBDIR/The quick brown fox jumps 311 0 file Jan 1 00:00:00.00 1980 /SUBDIR/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 312 0 #file Jan 1 00:00:00.00 1980 /SUBDIR/Locked 313 0 file Jan 1 00:00:00.00 1980 /SUBDIR/Hidden 314 0 file Jan 1 00:00:00.00 1980 /SUBDIR/System 315 0 file Jan 1 00:00:00.00 1980 /SUBDIR/Start of Cluster 316 0 file Jan 1 00:00:00.00 1980 /SUBDIR/Crossing a Cluster 317 0 file Jan 1 00:00:00.00 1980 /SUBDIR/End of Cluster 318 4096 dir Jan 1 00:00:00.00 1980 /Dates 319 0 file Oct 31 17:00:00.00 2013 /Dates/Halloween 320 0 file Jan 1 00:00:00.00 1980 /Dates/Valentine's 321 0 file Oct 17 17:04:00.00 1989 /Dates/Loma Prieta 322 0 file Jan 1 00:00:00.00 1980 /UPPER.TXT 323 0 file Jan 1 00:00:00.00 1980 /lower.txt 324 0 file Jan 1 00:00:00.00 1980 /mixed.TXT 325 0 file Jan 1 00:00:00.00 1980 /MyFile.txt 326 0 file Jan 1 00:00:00.00 1980 /abcdefghijklmnopqrstuvwxyz 327 0 file Jan 1 00:00:00.00 1980 /The quick brown fox jumps 328 0 file Jan 1 00:00:00.00 1980 /xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 329 0 #file Jan 1 00:00:00.00 1980 /Locked 330 0 file Jan 1 00:00:00.00 1980 /Hidden 331 0 file Jan 1 00:00:00.00 1980 /System 332""" 333 self.runTool(args, 0, expectedOut, '') 334 335 def testCaseInsensitiveLookup(self): 336 args = '--info /upper.txt /LOWER.TXT /MiXeD.TxT /myfile.txt'.split() 337 args.append('/The Quick Brown Fox Jumps') 338 args.append('/locked') 339 340 # TODO: Should we enforce that the printed name be the same case as on-disk? 341 expectedOut = """\ 342 0 file Jan 1 00:00:00.00 1980 /upper.txt 343 0 file Jan 1 00:00:00.00 1980 /LOWER.TXT 344 0 file Jan 1 00:00:00.00 1980 /MiXeD.TxT 345 0 file Jan 1 00:00:00.00 1980 /myfile.txt 346 0 file Jan 1 00:00:00.00 1980 /The Quick Brown Fox Jumps 347 0 #file Jan 1 00:00:00.00 1980 /locked 348""" 349 self.runTool(args, 0, expectedOut, '') 350 351 def testDates(self): 352 args = '--list --dates /Dates'.split() 353 expectedOut = """\ 354 0 file Jan 1 00:00:00.00 1980 Oct 31 17:00:00.00 2013 /Dates/Halloween 355 0 file Feb 14 22:33:44.55 2013 Jan 1 00:00:00.00 1980 /Dates/Valentine's 356 0 file Oct 17 17:04:06.90 1989 Oct 17 17:04:00.00 1989 /Dates/Loma Prieta 357""" 358 self.runTool(args, 0, expectedOut, '') 359 360class TestDelete(UserFSTestCase): 361 volumeName = 'DELETE' 362 363 # 364 # Because we'll be modifying the test volume, we need to be careful that 365 # the test cases get executed in a specific order so that our expected 366 # output is correct. To make the order obvious and predictable, the 367 # test case names have a number to control the ordering (since tests are 368 # run in order by name). 369 # 370 # * Deleting Files 371 # - initial state 372 # - existing file 373 # + make sure it's gone afterwards 374 # - missing file 375 # - same file twice (two separate userfs_tool invocations) 376 # - same file twice (one userfs_tool invocation) 377 # - multiple files 378 # - directory 379 # + make sure it's still there 380 # - locked file 381 # + make sure it's still there 382 # - file with multiple clusters in same FAT block 383 # - file with clusters in multiple FAT blocks 384 # - empty file 385 # - Long name, long entries at start of cluster 386 # - Long name, long entries cross cluster boundary 387 # - Short name 388 # - File inside subdirectory 389 # - final state 390 # ? recursive 391 392 @classmethod 393 def prepareContent(klass, volume): 394 bytesPerCluster = volume.bytesPerCluster 395 slotsPerCluster = bytesPerCluster / 32 396 397 root = volume.root() 398 root.mkfile('Empty') 399 root.mkfile('Twice1', content='Delete me twice') 400 root.mkfile('Multi1', content='Multi1') 401 root.mkfile('Multi2', content='Multi2') 402 root.mkfile('Twice2', content='Delete me twice in a single call') 403 root.mkfile('Existing', content='Existing') 404 root.mkfile('Locked', content='Locked', attributes=ATTR_ARCHIVE | ATTR_READ_ONLY) 405 root.mkfile('Keep Me', content='Keep Me') 406 root.mkfile('This file has a long Unicode name', content='This file has a long Unicode name') 407 408 # Make a subdirectory and a simple file inside it 409 clusters = volume.fat.find(2) 410 sub = root.mkdir('Subdirectory', clusters=clusters) 411 sub.mkfile('Child', content='Child') 412 413 # Make a file whose directory entries cross a cluster boundary 414 clusters = volume.fat.find(1) 415 volume.fat.chain(clusters) 416 if volume.fsinfo: 417 volume.fsinfo.allocate(1) 418 raw = make_long_dirent("Crossing Cluster Boundary", ATTR_ARCHIVE, head=clusters[0], length=25) 419 slot = slotsPerCluster - 1 420 sub.fill_slots(slot) 421 sub.write_slots(slot, raw) 422 crossing = volume.Chain(volume, clusters[0], 25) 423 crossing.pwrite(0, "Crossing Cluster Boundary") 424 425 root.mkfile('Contiguous', content='I am a contiguous file!\n'*4321) 426 427 # Make a file with 3 "extents" of 2, 3, and 4 clusters 428 clusters = volume.fat.find(11) 429 del clusters[6] 430 del clusters[3] 431 volume.fat.chain(clusters) 432 if volume.fsinfo: 433 volume.fsinfo.allocate(len(clusters)) 434 length = len(clusters) * bytesPerCluster - 37 435 root.mkfile('Extents', length=length, clusters=clusters) 436 klass.extentsSize = length 437 438 # Make a file with clusters spread out on the disk 439 head = volume.fat.find(1)[0] 440 clusters = range(head, volume.maxcluster, 7777) 441 random.Random("Delete").shuffle(clusters) 442 volume.fat.chain(clusters) 443 if volume.fsinfo: 444 volume.fsinfo.allocate(len(clusters)) 445 length = len(clusters) * bytesPerCluster - 66 446 root.mkfile('Fragmented', length=length, clusters=clusters) 447 klass.fragmentedSize = length 448 449 def test00VerifyInitialState(self): 450 args = '--list --recursive /'.split() 451 expected = """\ 452 0 file Jan 1 00:00:00.00 1980 /Empty 453 15 file Jan 1 00:00:00.00 1980 /Twice1 454 6 file Jan 1 00:00:00.00 1980 /Multi1 455 6 file Jan 1 00:00:00.00 1980 /Multi2 456 32 file Jan 1 00:00:00.00 1980 /Twice2 457 8 file Jan 1 00:00:00.00 1980 /Existing 458 6 #file Jan 1 00:00:00.00 1980 /Locked 459 7 file Jan 1 00:00:00.00 1980 /Keep Me 460 33 file Jan 1 00:00:00.00 1980 /This file has a long Unicode name 461 8192 dir Jan 1 00:00:00.00 1980 /Subdirectory 462 5 file Jan 1 00:00:00.00 1980 /Subdirectory/Child 463 25 file Jan 1 00:00:00.00 1980 /Subdirectory/Crossing Cluster Boundary 464 103704 file Jan 1 00:00:00.00 1980 /Contiguous 465{0:12d} file Jan 1 00:00:00.00 1980 /Extents 466{1:12d} file Jan 1 00:00:00.00 1980 /Fragmented 467""".format(self.extentsSize, self.fragmentedSize) 468 self.runTool(args, 0, expected, '') 469 470# args = '--sha /Fragmented'.split() 471# expected = 'ed24feba8bcf0ec4aaaebcd8391981109ae0ec25 /Fragmented\n' 472# self.runTool(args, 0, expected, '') 473 474 def test01DeleteExistingFile(self): 475 args = '--delete /Existing'.split() 476 expected = '' 477 self.runTool(args, 0, expected, '') 478 479 args = '--info /Existing'.split() 480 expectedErr = 'userfs_tool: /Existing: The operation couldn’t be completed. No such file or directory\n' 481 self.runTool(args, 1, '', expectedErr) 482 483 def test02DeleteMissingFile(self): 484 args = '--delete /Missing'.split() 485 expectedErr = 'userfs_tool: /Missing: The operation couldn’t be completed. No such file or directory\n' 486 self.runTool(args, 1, '', expectedErr) 487 488 def test03DeleteFileTwice(self): 489 args = '--delete /Twice1'.split() 490 expected = '' 491 self.runTool(args, 0, expected, '') 492 493 args = '--delete /Twice1'.split() 494 expectedErr = 'userfs_tool: /Twice1: The operation couldn’t be completed. No such file or directory\n' 495 self.runTool(args, 1, '', expectedErr) 496 497 def test04DeleteFileTwiceInOneCall(self): 498 args = '--delete /Twice2 /Twice2'.split() 499 expectedErr = 'userfs_tool: /Twice2: The operation couldn’t be completed. No such file or directory\n' 500 self.runTool(args, 1, '', expectedErr) 501 502 args = '--info /Twice2'.split() 503 expectedErr = 'userfs_tool: /Twice2: The operation couldn’t be completed. No such file or directory\n' 504 self.runTool(args, 1, '', expectedErr) 505 506 def test05DeleteMultipleFiles(self): 507 args = '--delete /Multi1 /Multi2'.split() 508 expected = '' 509 self.runTool(args, 0, expected, '') 510 511 args = '--info /Multi1'.split() 512 expectedErr = 'userfs_tool: /Multi1: The operation couldn’t be completed. No such file or directory\n' 513 self.runTool(args, 1, '', expectedErr) 514 515 args = '--info /Multi2'.split() 516 expectedErr = 'userfs_tool: /Multi2: The operation couldn’t be completed. No such file or directory\n' 517 self.runTool(args, 1, '', expectedErr) 518 519 def test06DeleteDirectory(self): 520 args = '--delete /Subdirectory'.split() 521 expectedErr = 'userfs_tool: delete_item: The operation couldn’t be completed. Is a directory\n' 522 self.runTool(args, 1, '', expectedErr) 523 524 def test07DeleteLockedFile(self): 525 args = '--delete /Locked'.split() 526 expectedErr = 'userfs_tool: delete_item: The operation couldn’t be completed. Operation not permitted\n' 527 self.runTool(args, 1, '', expectedErr) 528 529 # Make sure the file still exists 530 args = '--info /Locked'.split() 531 expected = ' 6 #file Jan 1 00:00:00.00 1980 /Locked\n' 532 self.runTool(args, 0, expected, '') 533 534 def test09DeleteEmptyFile(self): 535 args = '--delete /Empty'.split() 536 expected = '' 537 self.runTool(args, 0, expected, '') 538 539 def test11DeleteFileInDir(self): 540 args = '--delete /Subdirectory/Child'.split() 541 expected = '' 542 self.runTool(args, 0, expected, '') 543 544 args = '--info /Subdirectory/Child'.split() 545 expectedErr = 'userfs_tool: /Subdirectory/Child: The operation couldn’t be completed. No such file or directory\n' 546 self.runTool(args, 1, '', expectedErr) 547 548 # Delete a file that contains multiple, contiguous clusters 549 def test12DeleteContiguousFile(self): 550 args = '--delete /Contiguous'.split() 551 expected = '' 552 self.runTool(args, 0, expected, '') 553 554 args = '--info /Contiguous'.split() 555 expectedErr = 'userfs_tool: /Contiguous: The operation couldn’t be completed. No such file or directory\n' 556 self.runTool(args, 1, '', expectedErr) 557 558 # Delete a file that contains multiple multi-cluster "extents" 559 def test13DeleteExtentsFile(self): 560 args = '--delete /Extents'.split() 561 expected = '' 562 self.runTool(args, 0, expected, '') 563 564 args = '--info /Extents'.split() 565 expectedErr = 'userfs_tool: /Extents: The operation couldn’t be completed. No such file or directory\n' 566 self.runTool(args, 1, '', expectedErr) 567 568 # Delete a file that contains clusters from multiple FAT blocks 569 def test14DeleteFragmentedFile(self): 570 args = '--delete /Fragmented'.split() 571 expected = '' 572 self.runTool(args, 0, expected, '') 573 574 args = '--info /Fragmented'.split() 575 expectedErr = 'userfs_tool: /Fragmented: The operation couldn’t be completed. No such file or directory\n' 576 self.runTool(args, 1, '', expectedErr) 577 578 # Delete a file whose directory entries cross a cluster boundary 579 def test15DeleteCrossingFile(self): 580 args = ['--delete', '/Subdirectory/Crossing Cluster Boundary'] 581 expected = '' 582 self.runTool(args, 0, expected, '') 583 584 args = ['--info', '/Subdirectory/Crossing Cluster Boundary'] 585 expectedErr = 'userfs_tool: /Subdirectory/Crossing Cluster Boundary: The operation couldn’t be completed. No such file or directory\n' 586 self.runTool(args, 1, '', expectedErr) 587 588 def test16DeleteLongName(self): 589 args = ['--delete', '/This file has a long Unicode name'] 590 expected = '' 591 self.runTool(args, 0, expected, '') 592 593 args = ['--info', '/This file has a long Unicode name'] 594 expectedErr = 'userfs_tool: /This file has a long Unicode name: The operation couldn’t be completed. No such file or directory\n' 595 self.runTool(args, 1, '', expectedErr) 596 597 def test99VerifyFinalState(self): 598 args = '--list --recursive /'.split() 599 expected = """\ 600 6 #file Jan 1 00:00:00.00 1980 /Locked 601 7 file Jan 1 00:00:00.00 1980 /Keep Me 602 8192 dir Jan 1 00:00:00.00 1980 /Subdirectory 603""" 604 self.runTool(args, 0, expected, '') 605 606if __name__ == "__main__": 607 # main(sys.argv) 608 import getopt 609 610 # 611 # The default fsck_exfat to run, and the default path to the sparse bundle 612 # disk image used for testing. 613 # 614 FSCK_PATH = None 615 VERBOSE = False 616 DIFFS = False 617 KEEP_IMAGE = False 618 BUILT_PRODUCTS_DIR = os.environ.get('BUILT_PRODUCTS_DIR', None) 619 TOOL_PATH = os.environ.get('TOOL_PATH', None) 620 TOOL_ENV = None 621 622 # 623 # Let the path to fsck_exfat and the path to the sparse bundle image be 624 # overridden on the command line. We also parse the options that 625 # unittest.main() parses; we just collect them and pass them on for 626 # unittest to handle. Note that options need to precede arguments 627 # because getopt stops parsing options once it finds a non-option argument. 628 # 629 # NOTE: The "-V" option sets our VERBOSE variable as well as passing "-v" 630 # to unittest.main. 631 # 632 # Hmm. I could probably have just subclassed unittest.TestProgram and 633 # overridden the parseArgs method to handle my additional arguments. But 634 # then I'd have to assign the globals to self.__class__.__module__ so the 635 # test cases could see them. Or perhaps get the values from environment 636 # variables. 637 # 638 # TODO: Need an option to use a locally built framework. Perhaps either 639 # via a local Xcode build, or buildit. 640 # 641 # TODO: Need an option to use a locally built userfs_tool. Perhaps either 642 # via a local Xcode build, or buildit. 643 # 644 # TODO: Would it be sufficient to just point at a root, and grab both tool 645 # and framework from that? Or perhaps default to using the 646 # $BUILT_PRODUCTS_DIR from the current working copy's Debug configuration? 647 # 648 argv = [sys.argv[0]] 649 options, args = getopt.getopt(sys.argv[1:], "hHvVq", 650 "help verbose quiet keep repair= fsck= dir= img= device=".split()) 651 for opt, value in options: 652 if opt == '--keep': 653 KEEP_IMAGE = True 654 elif opt == "--fsck": 655 FSCK_PATH = value 656 elif opt == "--dir": 657 UserFSTestCase.imagePath = os.path.join(value, "FAT_TestCase") 658 elif opt == "--img": 659 UserFSTestCase.imagePath = value 660 elif opt == "--device": 661 UserFSTestCase.device = value 662 UserFSTestCase.imagePath = None 663 elif opt == "-V": 664 VERBOSE = True 665 DIFFS = True 666 argv.append("-v") 667 elif opt == "-v": 668 DIFFS = True 669 argv.append(opt) 670 else: 671 # NOTE: the normal unittest options don't have arguments 672 argv.append(opt) 673 argv.extend(args) 674 675 # 676 # Since this script is meant primarily for pre-submission testing by the 677 # engineer making the change, assume the engineer has built the Debug 678 # configuration, and wants to use that built version of the framework 679 # and tool. 680 # 681 if BUILT_PRODUCTS_DIR is None: 682 args = "xcodebuild -scheme All_iOS -configuration Debug -showBuildSettings".split() 683 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 684 stdout, stderr = p.communicate() 685 assert(p.returncode == 0) 686 687 BUILT_PRODUCTS_DIR = None 688 setting = " BUILT_PRODUCTS_DIR = " 689 for line in stdout.splitlines(): 690 if line.startswith(setting): 691 BUILT_PRODUCTS_DIR = line[len(setting):] # Delete setting name from start of string 692 assert(BUILT_PRODUCTS_DIR is not None) 693 BUILT_PRODUCTS_DIR = BUILT_PRODUCTS_DIR.rstrip("/") 694 695 if TOOL_PATH is None: 696 TOOL_PATH = os.path.join(BUILT_PRODUCTS_DIR, 'userfs_tool') 697 if TOOL_ENV is None: 698 TOOL_ENV = dict(DYLD_FRAMEWORK_PATH=BUILT_PRODUCTS_DIR, TZ="PST8PDT7") 699 700 # If we don't have a path to fsck_exfat, and it was built, use it. 701 if FSCK_PATH is None: 702 path = os.path.join(BUILT_PRODUCTS_DIR, 'fsck_msdos') 703 if os.path.isfile(path): 704 FSCK_PATH = path 705 706 # If no fsck_exfat was found/given, then default to the built-in one. 707 if FSCK_PATH is None: 708 FSCK_PATH = '/sbin/fsck_msdos' 709 710 unittest.main(argv=argv) 711