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