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 errno
30import os
31import re
32
33from webkitpy.common.system import path
34from webkitpy.common.system import ospath
35
36
37class MockFileSystem(object):
38    def __init__(self, files=None, cwd='/'):
39        """Initializes a "mock" filesystem that can be used to completely
40        stub out a filesystem.
41
42        Args:
43            files: a dict of filenames -> file contents. A file contents
44                value of None is used to indicate that the file should
45                not exist.
46        """
47        self.files = files or {}
48        self.written_files = {}
49        self._sep = '/'
50        self.current_tmpno = 0
51        self.cwd = cwd
52        self.dirs = {}
53
54    def _get_sep(self):
55        return self._sep
56
57    sep = property(_get_sep, doc="pathname separator")
58
59    def _raise_not_found(self, path):
60        raise IOError(errno.ENOENT, path, os.strerror(errno.ENOENT))
61
62    def _split(self, path):
63        return path.rsplit(self.sep, 1)
64
65    def abspath(self, path):
66        if os.path.isabs(path):
67            return self.normpath(path)
68        return self.abspath(self.join(self.cwd, path))
69
70    def basename(self, path):
71        return self._split(path)[1]
72
73    def chdir(self, path):
74        path = self.normpath(path)
75        if not self.isdir(path):
76            raise OSError(errno.ENOENT, path, os.strerror(errno.ENOENT))
77        self.cwd = path
78
79    def copyfile(self, source, destination):
80        if not self.exists(source):
81            self._raise_not_found(source)
82        if self.isdir(source):
83            raise IOError(errno.EISDIR, source, os.strerror(errno.ISDIR))
84        if self.isdir(destination):
85            raise IOError(errno.EISDIR, destination, os.strerror(errno.ISDIR))
86
87        self.files[destination] = self.files[source]
88        self.written_files[destination] = self.files[source]
89
90    def dirname(self, path):
91        return self._split(path)[0]
92
93    def exists(self, path):
94        return self.isfile(path) or self.isdir(path)
95
96    def files_under(self, path, dirs_to_skip=[], file_filter=None):
97        def filter_all(fs, dirpath, basename):
98            return True
99
100        file_filter = file_filter or filter_all
101        files = []
102        if self.isfile(path):
103            if file_filter(self, self.dirname(path), self.basename(path)):
104                files.append(path)
105            return files
106
107        if self.basename(path) in dirs_to_skip:
108            return []
109
110        if not path.endswith(self.sep):
111            path += self.sep
112
113        dir_substrings = [self.sep + d + self.sep for d in dirs_to_skip]
114        for filename in self.files:
115            if not filename.startswith(path):
116                continue
117
118            suffix = filename[len(path) - 1:]
119            if any(dir_substring in suffix for dir_substring in dir_substrings):
120                continue
121
122            dirpath, basename = self._split(filename)
123            if file_filter(self, dirpath, basename):
124                files.append(filename)
125
126        return files
127
128    def getcwd(self, path):
129        return self.cwd
130
131    def glob(self, path):
132        # FIXME: This only handles a wildcard '*' at the end of the path.
133        # Maybe it should handle more?
134        if path[-1] == '*':
135            return [f for f in self.files if f.startswith(path[:-1])]
136        else:
137            return [f for f in self.files if f == path]
138
139    def isabs(self, path):
140        return path.startswith(self.sep)
141
142    def isfile(self, path):
143        return path in self.files and self.files[path] is not None
144
145    def isdir(self, path):
146        if path in self.files:
147            return False
148        path = self.normpath(path)
149        if path in self.dirs:
150            return True
151
152        # We need to use a copy of the keys here in order to avoid switching
153        # to a different thread and potentially modifying the dict in
154        # mid-iteration.
155        files = self.files.keys()[:]
156        result = any(f.startswith(path) for f in files)
157        if result:
158            self.dirs[path] = True
159        return result
160
161    def join(self, *comps):
162        # FIXME: might want tests for this and/or a better comment about how
163        # it works.
164        return re.sub(re.escape(os.path.sep), self.sep, os.path.join(*comps))
165
166    def listdir(self, path):
167        if not self.isdir(path):
168            raise OSError("%s is not a directory" % path)
169
170        if not path.endswith(self.sep):
171            path += self.sep
172
173        dirs = []
174        files = []
175        for f in self.files:
176            if self.exists(f) and f.startswith(path):
177                remaining = f[len(path):]
178                if self.sep in remaining:
179                    dir = remaining[:remaining.index(self.sep)]
180                    if not dir in dirs:
181                        dirs.append(dir)
182                else:
183                    files.append(remaining)
184        return dirs + files
185
186    def mtime(self, path):
187        if self.exists(path):
188            return 0
189        self._raise_not_found(path)
190
191    def _mktemp(self, suffix='', prefix='tmp', dir=None, **kwargs):
192        if dir is None:
193            dir = self.sep + '__im_tmp'
194        curno = self.current_tmpno
195        self.current_tmpno += 1
196        return self.join(dir, "%s_%u_%s" % (prefix, curno, suffix))
197
198    def mkdtemp(self, **kwargs):
199        class TemporaryDirectory(object):
200            def __init__(self, fs, **kwargs):
201                self._kwargs = kwargs
202                self._filesystem = fs
203                self._directory_path = fs._mktemp(**kwargs)
204                fs.maybe_make_directory(self._directory_path)
205
206            def __str__(self):
207                return self._directory_path
208
209            def __enter__(self):
210                return self._directory_path
211
212            def __exit__(self, type, value, traceback):
213                # Only self-delete if necessary.
214
215                # FIXME: Should we delete non-empty directories?
216                if self._filesystem.exists(self._directory_path):
217                    self._filesystem.rmtree(self._directory_path)
218
219        return TemporaryDirectory(fs=self, **kwargs)
220
221    def maybe_make_directory(self, *path):
222        norm_path = self.normpath(self.join(*path))
223        if not self.isdir(norm_path):
224            self.dirs[norm_path] = True
225
226    def move(self, source, destination):
227        if self.files[source] is None:
228            self._raise_not_found(source)
229        self.files[destination] = self.files[source]
230        self.written_files[destination] = self.files[destination]
231        self.files[source] = None
232        self.written_files[source] = None
233
234    def normpath(self, path):
235        # Like join(), relies on os.path functionality but normalizes the
236        # path separator to the mock one.
237        return re.sub(re.escape(os.path.sep), self.sep, os.path.normpath(path))
238
239    def open_binary_tempfile(self, suffix=''):
240        path = self._mktemp(suffix)
241        return (WritableFileObject(self, path), path)
242
243    def open_text_file_for_writing(self, path, append=False):
244        return WritableFileObject(self, path, append)
245
246    def read_text_file(self, path):
247        return self.read_binary_file(path).decode('utf-8')
248
249    def open_binary_file_for_reading(self, path):
250        if self.files[path] is None:
251            self._raise_not_found(path)
252        return ReadableFileObject(self, path, self.files[path])
253
254    def read_binary_file(self, path):
255        # Intentionally raises KeyError if we don't recognize the path.
256        if self.files[path] is None:
257            self._raise_not_found(path)
258        return self.files[path]
259
260    def relpath(self, path, start='.'):
261        return ospath.relpath(path, start, self.abspath, self.sep)
262
263    def remove(self, path):
264        if self.files[path] is None:
265            self._raise_not_found(path)
266        self.files[path] = None
267        self.written_files[path] = None
268
269    def rmtree(self, path):
270        if not path.endswith(self.sep):
271            path += self.sep
272
273        for f in self.files:
274            if f.startswith(path):
275                self.files[f] = None
276
277    def splitext(self, path):
278        idx = path.rfind('.')
279        if idx == -1:
280            idx = 0
281        return (path[0:idx], path[idx:])
282
283    def write_text_file(self, path, contents):
284        return self.write_binary_file(path, contents.encode('utf-8'))
285
286    def write_binary_file(self, path, contents):
287        self.files[path] = contents
288        self.written_files[path] = contents
289
290
291class WritableFileObject(object):
292    def __init__(self, fs, path, append=False, encoding=None):
293        self.fs = fs
294        self.path = path
295        self.closed = False
296        if path not in self.fs.files or not append:
297            self.fs.files[path] = ""
298
299    def __enter__(self):
300        return self
301
302    def __exit__(self, type, value, traceback):
303        self.close()
304
305    def close(self):
306        self.closed = True
307
308    def write(self, str):
309        self.fs.files[self.path] += str
310        self.fs.written_files[self.path] = self.fs.files[self.path]
311
312
313class ReadableFileObject(object):
314    def __init__(self, fs, path, data=""):
315        self.fs = fs
316        self.path = path
317        self.closed = False
318        self.data = data
319        self.offset = 0
320
321    def __enter__(self):
322        return self
323
324    def __exit__(self, type, value, traceback):
325        self.close()
326
327    def close(self):
328        self.closed = True
329
330    def read(self, bytes=None):
331        if not bytes:
332            return self.data[self.offset:]
333        start = self.offset
334        self.offset += bytes
335        return self.data[start:self.offset]
336