1 # Copyright (C) 2009 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the name of Google Inc. nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 import StringIO 30 import errno 31 import hashlib 32 import os 33 import re 34 35 from webkitpy.common.system import path 36 37 38 class MockFileSystem(object): 39 sep = '/' 40 pardir = '..' 41 42 def __init__(self, files=None, dirs=None, cwd='/'): 43 """Initializes a "mock" filesystem that can be used to completely 44 stub out a filesystem. 45 46 Args: 47 files: a dict of filenames -> file contents. A file contents 48 value of None is used to indicate that the file should 49 not exist. 50 """ 51 self.files = files or {} 52 self.written_files = {} 53 self.last_tmpdir = None 54 self.current_tmpno = 0 55 self.cwd = cwd 56 self.dirs = set(dirs or []) 57 self.dirs.add(cwd) 58 for f in self.files: 59 d = self.dirname(f) 60 while not d in self.dirs: 61 self.dirs.add(d) 62 d = self.dirname(d) 63 64 def clear_written_files(self): 65 # This function can be used to track what is written between steps in a test. 66 self.written_files = {} 67 68 def _raise_not_found(self, path): 69 raise IOError(errno.ENOENT, path, os.strerror(errno.ENOENT)) 70 71 def _split(self, path): 72 # This is not quite a full implementation of os.path.split 73 # http://docs.python.org/library/os.path.html#os.path.split 74 if self.sep in path: 75 return path.rsplit(self.sep, 1) 76 return ('', path) 77 78 def abspath(self, path): 79 if os.path.isabs(path): 80 return self.normpath(path) 81 return self.abspath(self.join(self.cwd, path)) 82 83 def realpath(self, path): 84 return self.abspath(path) 85 86 def basename(self, path): 87 return self._split(path)[1] 88 89 def expanduser(self, path): 90 if path[0] != "~": 91 return path 92 parts = path.split(self.sep, 1) 93 home_directory = self.sep + "Users" + self.sep + "mock" 94 if len(parts) == 1: 95 return home_directory 96 return home_directory + self.sep + parts[1] 97 98 def path_to_module(self, module_name): 99 return "/mock-checkout/third_party/WebKit/Tools/Scripts/" + module_name.replace('.', '/') + ".py" 100 101 def chdir(self, path): 102 path = self.normpath(path) 103 if not self.isdir(path): 104 raise OSError(errno.ENOENT, path, os.strerror(errno.ENOENT)) 105 self.cwd = path 106 107 def copyfile(self, source, destination): 108 if not self.exists(source): 109 self._raise_not_found(source) 110 if self.isdir(source): 111 raise IOError(errno.EISDIR, source, os.strerror(errno.EISDIR)) 112 if self.isdir(destination): 113 raise IOError(errno.EISDIR, destination, os.strerror(errno.EISDIR)) 114 if not self.exists(self.dirname(destination)): 115 raise IOError(errno.ENOENT, destination, os.strerror(errno.ENOENT)) 116 117 self.files[destination] = self.files[source] 118 self.written_files[destination] = self.files[source] 119 120 def dirname(self, path): 121 return self._split(path)[0] 122 123 def exists(self, path): 124 return self.isfile(path) or self.isdir(path) 125 126 def files_under(self, path, dirs_to_skip=[], file_filter=None): 127 def filter_all(fs, dirpath, basename): 128 return True 129 130 file_filter = file_filter or filter_all 131 files = [] 132 if self.isfile(path): 133 if file_filter(self, self.dirname(path), self.basename(path)) and self.files[path] is not None: 134 files.append(path) 135 return files 136 137 if self.basename(path) in dirs_to_skip: 138 return [] 139 140 if not path.endswith(self.sep): 141 path += self.sep 142 143 dir_substrings = [self.sep + d + self.sep for d in dirs_to_skip] 144 for filename in self.files: 145 if not filename.startswith(path): 146 continue 147 148 suffix = filename[len(path) - 1:] 149 if any(dir_substring in suffix for dir_substring in dir_substrings): 150 continue 151 152 dirpath, basename = self._split(filename) 153 if file_filter(self, dirpath, basename) and self.files[filename] is not None: 154 files.append(filename) 155 156 return files 157 158 def getcwd(self): 159 return self.cwd 160 161 def glob(self, glob_string): 162 # FIXME: This handles '*', but not '?', '[', or ']'. 163 glob_string = re.escape(glob_string) 164 glob_string = glob_string.replace('\\*', '[^\\/]*') + '$' 165 glob_string = glob_string.replace('\\/', '/') 166 path_filter = lambda path: re.match(glob_string, path) 167 168 # We could use fnmatch.fnmatch, but that might not do the right thing on windows. 169 existing_files = [path for path, contents in self.files.items() if contents is not None] 170 return filter(path_filter, existing_files) + filter(path_filter, self.dirs) 171 172 def isabs(self, path): 173 return path.startswith(self.sep) 174 175 def isfile(self, path): 176 return path in self.files and self.files[path] is not None 177 178 def isdir(self, path): 179 return self.normpath(path) in self.dirs 180 181 def _slow_but_correct_join(self, *comps): 182 return re.sub(re.escape(os.path.sep), self.sep, os.path.join(*comps)) 183 184 def join(self, *comps): 185 # This function is called a lot, so we optimize it; there are 186 # unittests to check that we match _slow_but_correct_join(), above. 187 path = '' 188 sep = self.sep 189 for comp in comps: 190 if not comp: 191 continue 192 if comp[0] == sep: 193 path = comp 194 continue 195 if path: 196 path += sep 197 path += comp 198 if comps[-1] == '' and path: 199 path += '/' 200 path = path.replace(sep + sep, sep) 201 return path 202 203 def listdir(self, path): 204 root, dirs, files = list(self.walk(path))[0] 205 return dirs + files 206 207 def walk(self, top): 208 sep = self.sep 209 if not self.isdir(top): 210 raise OSError("%s is not a directory" % top) 211 212 if not top.endswith(sep): 213 top += sep 214 215 dirs = [] 216 files = [] 217 for f in self.files: 218 if self.exists(f) and f.startswith(top): 219 remaining = f[len(top):] 220 if sep in remaining: 221 dir = remaining[:remaining.index(sep)] 222 if not dir in dirs: 223 dirs.append(dir) 224 else: 225 files.append(remaining) 226 return [(top[:-1], dirs, files)] 227 228 def mtime(self, path): 229 if self.exists(path): 230 return 0 231 self._raise_not_found(path) 232 233 def _mktemp(self, suffix='', prefix='tmp', dir=None, **kwargs): 234 if dir is None: 235 dir = self.sep + '__im_tmp' 236 curno = self.current_tmpno 237 self.current_tmpno += 1 238 self.last_tmpdir = self.join(dir, '%s_%u_%s' % (prefix, curno, suffix)) 239 return self.last_tmpdir 240 241 def mkdtemp(self, **kwargs): 242 class TemporaryDirectory(object): 243 def __init__(self, fs, **kwargs): 244 self._kwargs = kwargs 245 self._filesystem = fs 246 self._directory_path = fs._mktemp(**kwargs) 247 fs.maybe_make_directory(self._directory_path) 248 249 def __str__(self): 250 return self._directory_path 251 252 def __enter__(self): 253 return self._directory_path 254 255 def __exit__(self, type, value, traceback): 256 # Only self-delete if necessary. 257 258 # FIXME: Should we delete non-empty directories? 259 if self._filesystem.exists(self._directory_path): 260 self._filesystem.rmtree(self._directory_path) 261 262 return TemporaryDirectory(fs=self, **kwargs) 263 264 def maybe_make_directory(self, *path): 265 norm_path = self.normpath(self.join(*path)) 266 while norm_path and not self.isdir(norm_path): 267 self.dirs.add(norm_path) 268 norm_path = self.dirname(norm_path) 269 270 def move(self, source, destination): 271 if self.files[source] is None: 272 self._raise_not_found(source) 273 self.files[destination] = self.files[source] 274 self.written_files[destination] = self.files[destination] 275 self.files[source] = None 276 self.written_files[source] = None 277 278 def _slow_but_correct_normpath(self, path): 279 return re.sub(re.escape(os.path.sep), self.sep, os.path.normpath(path)) 280 281 def normpath(self, path): 282 # This function is called a lot, so we try to optimize the common cases 283 # instead of always calling _slow_but_correct_normpath(), above. 284 if '..' in path or '/./' in path: 285 # This doesn't happen very often; don't bother trying to optimize it. 286 return self._slow_but_correct_normpath(path) 287 if not path: 288 return '.' 289 if path == '/': 290 return path 291 if path == '/.': 292 return '/' 293 if path.endswith('/.'): 294 return path[:-2] 295 if path.endswith('/'): 296 return path[:-1] 297 return path 298 299 def open_binary_tempfile(self, suffix=''): 300 path = self._mktemp(suffix) 301 return (WritableBinaryFileObject(self, path), path) 302 303 def open_binary_file_for_reading(self, path): 304 if self.files[path] is None: 305 self._raise_not_found(path) 306 return ReadableBinaryFileObject(self, path, self.files[path]) 307 308 def read_binary_file(self, path): 309 # Intentionally raises KeyError if we don't recognize the path. 310 if self.files[path] is None: 311 self._raise_not_found(path) 312 return self.files[path] 313 314 def write_binary_file(self, path, contents): 315 # FIXME: should this assert if dirname(path) doesn't exist? 316 self.maybe_make_directory(self.dirname(path)) 317 self.files[path] = contents 318 self.written_files[path] = contents 319 320 def open_text_file_for_reading(self, path): 321 if self.files[path] is None: 322 self._raise_not_found(path) 323 return ReadableTextFileObject(self, path, self.files[path]) 324 325 def open_text_file_for_writing(self, path): 326 return WritableTextFileObject(self, path) 327 328 def read_text_file(self, path): 329 return self.read_binary_file(path).decode('utf-8') 330 331 def write_text_file(self, path, contents): 332 return self.write_binary_file(path, contents.encode('utf-8')) 333 334 def sha1(self, path): 335 contents = self.read_binary_file(path) 336 return hashlib.sha1(contents).hexdigest() 337 338 def relpath(self, path, start='.'): 339 # Since os.path.relpath() calls os.path.normpath() 340 # (see http://docs.python.org/library/os.path.html#os.path.abspath ) 341 # it also removes trailing slashes and converts forward and backward 342 # slashes to the preferred slash os.sep. 343 start = self.abspath(start) 344 path = self.abspath(path) 345 346 common_root = start 347 dot_dot = '' 348 while not common_root == '': 349 if path.startswith(common_root): 350 break 351 common_root = self.dirname(common_root) 352 dot_dot += '..' + self.sep 353 354 rel_path = path[len(common_root):] 355 356 if not rel_path: 357 return '.' 358 359 if rel_path[0] == self.sep: 360 # It is probably sufficient to remove just the first character 361 # since os.path.normpath() collapses separators, but we use 362 # lstrip() just to be sure. 363 rel_path = rel_path.lstrip(self.sep) 364 elif not common_root == '/': 365 # We are in the case typified by the following example: 366 # path = "/tmp/foobar", start = "/tmp/foo" -> rel_path = "bar" 367 common_root = self.dirname(common_root) 368 dot_dot += '..' + self.sep 369 rel_path = path[len(common_root) + 1:] 370 371 return dot_dot + rel_path 372 373 def remove(self, path): 374 if self.files[path] is None: 375 self._raise_not_found(path) 376 self.files[path] = None 377 self.written_files[path] = None 378 379 def rmtree(self, path): 380 path = self.normpath(path) 381 382 for f in self.files: 383 # We need to add a trailing separator to path to avoid matching 384 # cases like path='/foo/b' and f='/foo/bar/baz'. 385 if f == path or f.startswith(path + self.sep): 386 self.files[f] = None 387 388 self.dirs = set(filter(lambda d: not (d == path or d.startswith(path + self.sep)), self.dirs)) 389 390 def copytree(self, source, destination): 391 source = self.normpath(source) 392 destination = self.normpath(destination) 393 394 for source_file in self.files: 395 if source_file.startswith(source): 396 destination_path = self.join(destination, self.relpath(source_file, source)) 397 self.maybe_make_directory(self.dirname(destination_path)) 398 self.files[destination_path] = self.files[source_file] 399 400 def split(self, path): 401 idx = path.rfind(self.sep) 402 if idx == -1: 403 return ('', path) 404 return (path[:idx], path[(idx + 1):]) 405 406 def splitext(self, path): 407 idx = path.rfind('.') 408 if idx == -1: 409 idx = len(path) 410 return (path[0:idx], path[idx:]) 411 412 413 class WritableBinaryFileObject(object): 414 def __init__(self, fs, path): 415 self.fs = fs 416 self.path = path 417 self.closed = False 418 self.fs.files[path] = "" 419 420 def __enter__(self): 421 return self 422 423 def __exit__(self, type, value, traceback): 424 self.close() 425 426 def close(self): 427 self.closed = True 428 429 def write(self, str): 430 self.fs.files[self.path] += str 431 self.fs.written_files[self.path] = self.fs.files[self.path] 432 433 434 class WritableTextFileObject(WritableBinaryFileObject): 435 def write(self, str): 436 WritableBinaryFileObject.write(self, str.encode('utf-8')) 437 438 439 class ReadableBinaryFileObject(object): 440 def __init__(self, fs, path, data): 441 self.fs = fs 442 self.path = path 443 self.closed = False 444 self.data = data 445 self.offset = 0 446 447 def __enter__(self): 448 return self 449 450 def __exit__(self, type, value, traceback): 451 self.close() 452 453 def close(self): 454 self.closed = True 455 456 def read(self, bytes=None): 457 if not bytes: 458 return self.data[self.offset:] 459 start = self.offset 460 self.offset += bytes 461 return self.data[start:self.offset] 462 463 464 class ReadableTextFileObject(ReadableBinaryFileObject): 465 def __init__(self, fs, path, data): 466 super(ReadableTextFileObject, self).__init__(fs, path, StringIO.StringIO(data.decode("utf-8"))) 467 468 def close(self): 469 self.data.close() 470 super(ReadableTextFileObject, self).close() 471 472 def read(self, bytes=-1): 473 return self.data.read(bytes) 474 475 def readline(self, length=None): 476 return self.data.readline(length) 477 478 def __iter__(self): 479 return self.data.__iter__() 480 481 def next(self): 482 return self.data.next() 483 484 def seek(self, offset, whence=os.SEEK_SET): 485 self.data.seek(offset, whence) 486