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 29import StringIO 30import errno 31import hashlib 32import os 33import re 34 35from webkitpy.common.system import path 36 37 38class 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 not self.exists(source): 272 self._raise_not_found(source) 273 if self.isfile(source): 274 self.files[destination] = self.files[source] 275 self.written_files[destination] = self.files[destination] 276 self.files[source] = None 277 self.written_files[source] = None 278 return 279 self.copytree(source, destination) 280 self.rmtree(source) 281 282 def _slow_but_correct_normpath(self, path): 283 return re.sub(re.escape(os.path.sep), self.sep, os.path.normpath(path)) 284 285 def normpath(self, path): 286 # This function is called a lot, so we try to optimize the common cases 287 # instead of always calling _slow_but_correct_normpath(), above. 288 if '..' in path or '/./' in path: 289 # This doesn't happen very often; don't bother trying to optimize it. 290 return self._slow_but_correct_normpath(path) 291 if not path: 292 return '.' 293 if path == '/': 294 return path 295 if path == '/.': 296 return '/' 297 if path.endswith('/.'): 298 return path[:-2] 299 if path.endswith('/'): 300 return path[:-1] 301 return path 302 303 def open_binary_tempfile(self, suffix=''): 304 path = self._mktemp(suffix) 305 return (WritableBinaryFileObject(self, path), path) 306 307 def open_binary_file_for_reading(self, path): 308 if self.files[path] is None: 309 self._raise_not_found(path) 310 return ReadableBinaryFileObject(self, path, self.files[path]) 311 312 def read_binary_file(self, path): 313 # Intentionally raises KeyError if we don't recognize the path. 314 if self.files[path] is None: 315 self._raise_not_found(path) 316 return self.files[path] 317 318 def write_binary_file(self, path, contents): 319 # FIXME: should this assert if dirname(path) doesn't exist? 320 self.maybe_make_directory(self.dirname(path)) 321 self.files[path] = contents 322 self.written_files[path] = contents 323 324 def open_text_file_for_reading(self, path): 325 if self.files[path] is None: 326 self._raise_not_found(path) 327 return ReadableTextFileObject(self, path, self.files[path]) 328 329 def open_text_file_for_writing(self, path): 330 return WritableTextFileObject(self, path) 331 332 def read_text_file(self, path): 333 return self.read_binary_file(path).decode('utf-8') 334 335 def write_text_file(self, path, contents): 336 return self.write_binary_file(path, contents.encode('utf-8')) 337 338 def sha1(self, path): 339 contents = self.read_binary_file(path) 340 return hashlib.sha1(contents).hexdigest() 341 342 def relpath(self, path, start='.'): 343 # Since os.path.relpath() calls os.path.normpath() 344 # (see http://docs.python.org/library/os.path.html#os.path.abspath ) 345 # it also removes trailing slashes and converts forward and backward 346 # slashes to the preferred slash os.sep. 347 start = self.abspath(start) 348 path = self.abspath(path) 349 350 common_root = start 351 dot_dot = '' 352 while not common_root == '': 353 if path.startswith(common_root): 354 break 355 common_root = self.dirname(common_root) 356 dot_dot += '..' + self.sep 357 358 rel_path = path[len(common_root):] 359 360 if not rel_path: 361 return '.' 362 363 if rel_path[0] == self.sep: 364 # It is probably sufficient to remove just the first character 365 # since os.path.normpath() collapses separators, but we use 366 # lstrip() just to be sure. 367 rel_path = rel_path.lstrip(self.sep) 368 elif not common_root == '/': 369 # We are in the case typified by the following example: 370 # path = "/tmp/foobar", start = "/tmp/foo" -> rel_path = "bar" 371 common_root = self.dirname(common_root) 372 dot_dot += '..' + self.sep 373 rel_path = path[len(common_root) + 1:] 374 375 return dot_dot + rel_path 376 377 def remove(self, path): 378 if self.files[path] is None: 379 self._raise_not_found(path) 380 self.files[path] = None 381 self.written_files[path] = None 382 383 def rmtree(self, path): 384 path = self.normpath(path) 385 386 for f in self.files: 387 # We need to add a trailing separator to path to avoid matching 388 # cases like path='/foo/b' and f='/foo/bar/baz'. 389 if f == path or f.startswith(path + self.sep): 390 self.files[f] = None 391 392 self.dirs = set(filter(lambda d: not (d == path or d.startswith(path + self.sep)), self.dirs)) 393 394 def copytree(self, source, destination): 395 source = self.normpath(source) 396 destination = self.normpath(destination) 397 398 for source_file in list(self.files): 399 if source_file.startswith(source): 400 destination_path = self.join(destination, self.relpath(source_file, source)) 401 self.maybe_make_directory(self.dirname(destination_path)) 402 self.files[destination_path] = self.files[source_file] 403 404 def split(self, path): 405 idx = path.rfind(self.sep) 406 if idx == -1: 407 return ('', path) 408 return (path[:idx], path[(idx + 1):]) 409 410 def splitext(self, path): 411 idx = path.rfind('.') 412 if idx == -1: 413 idx = len(path) 414 return (path[0:idx], path[idx:]) 415 416 417class WritableBinaryFileObject(object): 418 def __init__(self, fs, path): 419 self.fs = fs 420 self.path = path 421 self.closed = False 422 self.fs.files[path] = "" 423 424 def __enter__(self): 425 return self 426 427 def __exit__(self, type, value, traceback): 428 self.close() 429 430 def close(self): 431 self.closed = True 432 433 def write(self, str): 434 self.fs.files[self.path] += str 435 self.fs.written_files[self.path] = self.fs.files[self.path] 436 437 438class WritableTextFileObject(WritableBinaryFileObject): 439 def write(self, str): 440 WritableBinaryFileObject.write(self, str.encode('utf-8')) 441 442 443class ReadableBinaryFileObject(object): 444 def __init__(self, fs, path, data): 445 self.fs = fs 446 self.path = path 447 self.closed = False 448 self.data = data 449 self.offset = 0 450 451 def __enter__(self): 452 return self 453 454 def __exit__(self, type, value, traceback): 455 self.close() 456 457 def close(self): 458 self.closed = True 459 460 def read(self, bytes=None): 461 if not bytes: 462 return self.data[self.offset:] 463 start = self.offset 464 self.offset += bytes 465 return self.data[start:self.offset] 466 467 468class ReadableTextFileObject(ReadableBinaryFileObject): 469 def __init__(self, fs, path, data): 470 super(ReadableTextFileObject, self).__init__(fs, path, StringIO.StringIO(data.decode("utf-8"))) 471 472 def close(self): 473 self.data.close() 474 super(ReadableTextFileObject, self).close() 475 476 def read(self, bytes=-1): 477 return self.data.read(bytes) 478 479 def readline(self, length=None): 480 return self.data.readline(length) 481 482 def __iter__(self): 483 return self.data.__iter__() 484 485 def next(self): 486 return self.data.next() 487 488 def seek(self, offset, whence=os.SEEK_SET): 489 self.data.seek(offset, whence) 490