W3C test parser and converter should use test importer host
[WebKit-https.git] / Tools / Scripts / webkitpy / common / system / filesystem_mock.py
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/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 dirs_under(self, path, dirs_filter=None):
127         def filter_all(fs, dirpath):
128             return True
129
130         dirs_filter = dirs_filter or filter_all
131
132         dirs = []
133         for dirpath in self.dirs:
134             if not dirpath.startswith(path):
135                 continue
136             if dirs_filter(self, dirpath):
137                 dirs.append(dirpath)
138         return sorted(dirs)
139
140
141     def files_under(self, path, dirs_to_skip=[], file_filter=None):
142         def filter_all(fs, dirpath, basename):
143             return True
144
145         file_filter = file_filter or filter_all
146         files = []
147         if self.isfile(path):
148             if file_filter(self, self.dirname(path), self.basename(path)) and self.files[path] is not None:
149                 files.append(path)
150             return files
151
152         if self.basename(path) in dirs_to_skip:
153             return []
154
155         if not path.endswith(self.sep):
156             path += self.sep
157
158         dir_substrings = [self.sep + d + self.sep for d in dirs_to_skip]
159         for filename in self.files:
160             if not filename.startswith(path):
161                 continue
162
163             suffix = filename[len(path) - 1:]
164             if any(dir_substring in suffix for dir_substring in dir_substrings):
165                 continue
166
167             dirpath, basename = self._split(filename)
168             if file_filter(self, dirpath, basename) and self.files[filename] is not None:
169                 files.append(filename)
170
171         return files
172
173     def getcwd(self):
174         return self.cwd
175
176     def getsize(self, path):
177         if not self.isfile(path):
178             raise OSError("%s is not a file" % path)
179         return len(self.files[path])
180
181     def glob(self, glob_string):
182         # FIXME: This handles '*', but not '?', '[', or ']'.
183         glob_string = re.escape(glob_string)
184         glob_string = glob_string.replace('\\*', '[^\\/]*') + '$'
185         glob_string = glob_string.replace('\\/', '/')
186         path_filter = lambda path: re.match(glob_string, path)
187
188         # We could use fnmatch.fnmatch, but that might not do the right thing on windows.
189         existing_files = [path for path, contents in self.files.items() if contents is not None]
190         return filter(path_filter, existing_files) + filter(path_filter, self.dirs)
191
192     def isabs(self, path):
193         return path.startswith(self.sep)
194
195     def isfile(self, path):
196         return path in self.files and self.files[path] is not None
197
198     def isdir(self, path):
199         return self.normpath(path) in self.dirs
200
201     def _slow_but_correct_join(self, *comps):
202         return re.sub(re.escape(os.path.sep), self.sep, os.path.join(*comps))
203
204     def join(self, *comps):
205         # This function is called a lot, so we optimize it; there are
206         # unittests to check that we match _slow_but_correct_join(), above.
207         path = ''
208         sep = self.sep
209         for comp in comps:
210             if not comp:
211                 continue
212             if comp[0] == sep:
213                 path = comp
214                 continue
215             if path:
216                 path += sep
217             path += comp
218         if comps[-1] == '' and path:
219             path += '/'
220         path = path.replace(sep + sep, sep)
221         return path
222
223     def listdir(self, path):
224         sep = self.sep
225         if not self.isdir(path):
226             raise OSError("%s is not a directory" % path)
227
228         if not path.endswith(sep):
229             path += sep
230
231         dirs = []
232         files = []
233         for f in self.files:
234             if self.exists(f) and f.startswith(path):
235                 remaining = f[len(path):]
236                 if sep in remaining:
237                     dir = remaining[:remaining.index(sep)]
238                     if not dir in dirs:
239                         dirs.append(dir)
240                 else:
241                     files.append(remaining)
242         return dirs + files
243
244     def mtime(self, path):
245         if self.exists(path):
246             return 0
247         self._raise_not_found(path)
248
249     def _mktemp(self, suffix='', prefix='tmp', dir=None, **kwargs):
250         if dir is None:
251             dir = self.sep + '__im_tmp'
252         curno = self.current_tmpno
253         self.current_tmpno += 1
254         self.last_tmpdir = self.join(dir, '%s_%u_%s' % (prefix, curno, suffix))
255         return self.last_tmpdir
256
257     def mkdtemp(self, **kwargs):
258         class TemporaryDirectory(object):
259             def __init__(self, fs, **kwargs):
260                 self._kwargs = kwargs
261                 self._filesystem = fs
262                 self._directory_path = fs._mktemp(**kwargs)
263                 fs.maybe_make_directory(self._directory_path)
264
265             def __str__(self):
266                 return self._directory_path
267
268             def __enter__(self):
269                 return self._directory_path
270
271             def __exit__(self, type, value, traceback):
272                 # Only self-delete if necessary.
273
274                 # FIXME: Should we delete non-empty directories?
275                 if self._filesystem.exists(self._directory_path):
276                     self._filesystem.rmtree(self._directory_path)
277
278         return TemporaryDirectory(fs=self, **kwargs)
279
280     def maybe_make_directory(self, *path):
281         norm_path = self.normpath(self.join(*path))
282         while norm_path and not self.isdir(norm_path):
283             self.dirs.add(norm_path)
284             norm_path = self.dirname(norm_path)
285
286     def move(self, source, destination):
287         if self.files[source] is None:
288             self._raise_not_found(source)
289         self.files[destination] = self.files[source]
290         self.written_files[destination] = self.files[destination]
291         self.files[source] = None
292         self.written_files[source] = None
293
294     def _slow_but_correct_normpath(self, path):
295         return re.sub(re.escape(os.path.sep), self.sep, os.path.normpath(path))
296
297     def normpath(self, path):
298         # This function is called a lot, so we try to optimize the common cases
299         # instead of always calling _slow_but_correct_normpath(), above.
300         if '..' in path or '/./' in path:
301             # This doesn't happen very often; don't bother trying to optimize it.
302             return self._slow_but_correct_normpath(path)
303         if not path:
304             return '.'
305         if path == '/':
306             return path
307         if path == '/.':
308             return '/'
309         if path.endswith('/.'):
310             return path[:-2]
311         if path.endswith('/'):
312             return path[:-1]
313         return path
314
315     def open_binary_tempfile(self, suffix=''):
316         path = self._mktemp(suffix)
317         return (WritableBinaryFileObject(self, path), path)
318
319     def open_binary_file_for_reading(self, path):
320         if self.files[path] is None:
321             self._raise_not_found(path)
322         return ReadableBinaryFileObject(self, path, self.files[path])
323
324     def read_binary_file(self, path):
325         # Intentionally raises KeyError if we don't recognize the path.
326         if self.files[path] is None:
327             self._raise_not_found(path)
328         return self.files[path]
329
330     def write_binary_file(self, path, contents):
331         # FIXME: should this assert if dirname(path) doesn't exist?
332         self.maybe_make_directory(self.dirname(path))
333         self.files[path] = contents
334         self.written_files[path] = contents
335
336     def open_text_file_for_reading(self, path):
337         if self.files[path] is None:
338             self._raise_not_found(path)
339         return ReadableTextFileObject(self, path, self.files[path])
340
341     def open_text_file_for_writing(self, path):
342         return WritableTextFileObject(self, path)
343
344     def read_text_file(self, path):
345         return self.read_binary_file(path).decode('utf-8')
346
347     def write_text_file(self, path, contents):
348         return self.write_binary_file(path, contents.encode('utf-8'))
349
350     def sha1(self, path):
351         contents = self.read_binary_file(path)
352         return hashlib.sha1(contents).hexdigest()
353
354     def relpath(self, path, start='.'):
355         # Since os.path.relpath() calls os.path.normpath()
356         # (see http://docs.python.org/library/os.path.html#os.path.abspath )
357         # it also removes trailing slashes and converts forward and backward
358         # slashes to the preferred slash os.sep.
359         start = self.abspath(start)
360         path = self.abspath(path)
361
362         if not path.lower().startswith(start.lower()):
363             # path is outside the directory given by start; compute path from root
364             return '../' * start.count('/') + path
365
366         rel_path = path[len(start):]
367
368         if not rel_path:
369             # Then the paths are the same.
370             pass
371         elif rel_path[0] == self.sep:
372             # It is probably sufficient to remove just the first character
373             # since os.path.normpath() collapses separators, but we use
374             # lstrip() just to be sure.
375             rel_path = rel_path.lstrip(self.sep)
376         else:
377             # We are in the case typified by the following example:
378             # path = "/tmp/foobar", start = "/tmp/foo" -> rel_path = "bar"
379             # FIXME: We return a less-than-optimal result here.
380             return '../' * start.count('/') + path
381
382         return rel_path
383
384     def remove(self, path):
385         if self.files[path] is None:
386             self._raise_not_found(path)
387         self.files[path] = None
388         self.written_files[path] = None
389
390     def rmtree(self, path):
391         path = self.normpath(path)
392
393         for f in self.files:
394             if f.startswith(path):
395                 self.files[f] = None
396
397         self.dirs = set(filter(lambda d: not d.startswith(path), self.dirs))
398
399     def copytree(self, source, destination):
400         source = self.normpath(source)
401         destination = self.normpath(destination)
402
403         for source_file in list(self.files):
404             if source_file.startswith(source):
405                 destination_path = self.join(destination, self.relpath(source_file, source))
406                 self.maybe_make_directory(self.dirname(destination_path))
407                 self.files[destination_path] = self.files[source_file]
408
409     def split(self, path):
410         idx = path.rfind(self.sep)
411         if idx == -1:
412             return ('', path)
413         return (path[:idx], path[(idx + 1):])
414
415     def splitext(self, path):
416         idx = path.rfind('.')
417         if idx == -1:
418             idx = len(path)
419         return (path[0:idx], path[idx:])
420
421
422 class WritableBinaryFileObject(object):
423     def __init__(self, fs, path):
424         self.fs = fs
425         self.path = path
426         self.closed = False
427         self.fs.files[path] = ""
428
429     def __enter__(self):
430         return self
431
432     def __exit__(self, type, value, traceback):
433         self.close()
434
435     def close(self):
436         self.closed = True
437
438     def write(self, str):
439         self.fs.files[self.path] += str
440         self.fs.written_files[self.path] = self.fs.files[self.path]
441
442
443 class WritableTextFileObject(WritableBinaryFileObject):
444     def write(self, str):
445         WritableBinaryFileObject.write(self, str.encode('utf-8'))
446
447
448 class ReadableBinaryFileObject(object):
449     def __init__(self, fs, path, data):
450         self.fs = fs
451         self.path = path
452         self.closed = False
453         self.data = data
454         self.offset = 0
455
456     def __enter__(self):
457         return self
458
459     def __exit__(self, type, value, traceback):
460         self.close()
461
462     def close(self):
463         self.closed = True
464
465     def read(self, bytes=None):
466         if not bytes:
467             return self.data[self.offset:]
468         start = self.offset
469         self.offset += bytes
470         return self.data[start:self.offset]
471
472
473 class ReadableTextFileObject(ReadableBinaryFileObject):
474     def __init__(self, fs, path, data):
475         super(ReadableTextFileObject, self).__init__(fs, path, StringIO.StringIO(data.decode("utf-8")))
476
477     def close(self):
478         self.data.close()
479         super(ReadableTextFileObject, self).close()
480
481     def read(self, bytes=-1):
482         return self.data.read(bytes)
483
484     def readline(self, length=None):
485         return self.data.readline(length)
486
487     def __iter__(self):
488         return self.data.__iter__()
489
490     def next(self):
491         return self.data.next()
492
493     def seek(self, offset, whence=os.SEEK_SET):
494         self.data.seek(offset, whence)