OLD | NEW |
1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 from file_system import FileSystem, FileNotFoundError, StatInfo | 5 from file_system import FileSystem, FileNotFoundError, StatInfo |
6 from future import Future | 6 from future import Future |
| 7 from path_util import IsDirectory |
7 | 8 |
8 | 9 |
9 def MoveTo(base, obj): | 10 def MoveTo(base, obj): |
10 '''Returns an object as |obj| moved to |base|. That is, | 11 '''Returns an object as |obj| moved to |base|. That is, |
11 MoveTo('foo/bar', {'a': 'b'}) -> {'foo': {'bar': {'a': 'b'}}} | 12 MoveTo('foo/bar', {'a': 'b'}) -> {'foo': {'bar': {'a': 'b'}}} |
12 ''' | 13 ''' |
13 result = {} | 14 result = {} |
14 leaf = result | 15 leaf = result |
15 for k in base.split('/'): | 16 for k in base.split('/'): |
16 leaf[k] = {} | 17 leaf[k] = {} |
17 leaf = leaf[k] | 18 leaf = leaf[k] |
18 leaf.update(obj) | 19 leaf.update(obj) |
19 return result | 20 return result |
20 | 21 |
21 | 22 |
22 def MoveAllTo(base, obj): | 23 def MoveAllTo(base, obj): |
23 '''Moves every value in |obj| to |base|. See MoveTo. | 24 '''Moves every value in |obj| to |base|. See MoveTo. |
24 ''' | 25 ''' |
25 result = {} | 26 result = {} |
26 for key, value in obj.iteritems(): | 27 for key, value in obj.iteritems(): |
27 result[key] = MoveTo(base, value) | 28 result[key] = MoveTo(base, value) |
28 return result | 29 return result |
29 | 30 |
30 | 31 |
| 32 def _List(file_system): |
| 33 '''Returns a list of '/' separated paths derived from |file_system|. |
| 34 For example, {'index.html': '', 'www': {'file.txt': ''}} would return |
| 35 ['index.html', 'www/file.txt']. |
| 36 ''' |
| 37 assert isinstance(file_system, dict) |
| 38 result = {} |
| 39 def update_result(item, path): |
| 40 if isinstance(item, dict): |
| 41 if path != '': |
| 42 path += '/' |
| 43 result[path] = [p if isinstance(content, basestring) else (p + '/') |
| 44 for p, content in item.iteritems()] |
| 45 for subpath, subitem in item.iteritems(): |
| 46 update_result(subitem, path + subpath) |
| 47 elif isinstance(item, basestring): |
| 48 result[path] = item |
| 49 else: |
| 50 raise ValueError('Unsupported item type: %s' % type(item)) |
| 51 update_result(file_system, '') |
| 52 return result |
| 53 |
| 54 |
| 55 class _StatTracker(object): |
| 56 '''Maintains the versions of paths in a file system. The versions of files |
| 57 are changed either by |Increment| or |SetVersion|. The versions of |
| 58 directories are derived from the versions of files within it. |
| 59 ''' |
| 60 |
| 61 def __init__(self): |
| 62 self._path_stats = {} |
| 63 self._global_stat = 0 |
| 64 |
| 65 def Increment(self, path=None, by=1): |
| 66 if path is None: |
| 67 self._global_stat += by |
| 68 else: |
| 69 self.SetVersion(path, self._path_stats.get(path, 0) + by) |
| 70 |
| 71 def SetVersion(self, path, new_version): |
| 72 if IsDirectory(path): |
| 73 raise ValueError('Only files have an incrementable stat, ' |
| 74 'but "%s" is a directory' % path) |
| 75 |
| 76 # Update version of that file. |
| 77 self._path_stats[path] = new_version |
| 78 |
| 79 # Update all parent directory versions as well. |
| 80 slash_index = 0 # (deliberately including '' in the dir paths) |
| 81 while slash_index != -1: |
| 82 dir_path = path[:slash_index] + '/' |
| 83 self._path_stats[dir_path] = max(self._path_stats.get(dir_path, 0), |
| 84 new_version) |
| 85 if dir_path == '/': |
| 86 # Legacy support for '/' being the root of the file system rather |
| 87 # than ''. Eventually when the path normalisation logic is complete |
| 88 # this will be impossible and this logic will change slightly. |
| 89 self._path_stats[''] = self._path_stats['/'] |
| 90 slash_index = path.find('/', slash_index + 1) |
| 91 |
| 92 def GetVersion(self, path): |
| 93 return self._global_stat + self._path_stats.get(path, 0) |
| 94 |
| 95 |
31 class TestFileSystem(FileSystem): | 96 class TestFileSystem(FileSystem): |
32 '''A FileSystem backed by an object. Create with an object representing file | 97 '''A FileSystem backed by an object. Create with an object representing file |
33 paths such that {'a': {'b': 'hello'}} will resolve Read('a/b') as 'hello', | 98 paths such that {'a': {'b': 'hello'}} will resolve Read('a/b') as 'hello', |
34 Read('a/') as ['b'], and Stat determined by a value incremented via | 99 Read('a/') as ['b'], and Stat determined by a value incremented via |
35 IncrementStat. | 100 IncrementStat. |
36 ''' | 101 ''' |
37 | 102 |
38 def __init__(self, obj, relative_to=None, identity=None): | 103 def __init__(self, obj, relative_to=None, identity=None): |
39 assert obj is not None | 104 assert obj is not None |
40 self._obj = obj if relative_to is None else MoveTo(relative_to, obj) | 105 if relative_to is not None: |
| 106 obj = MoveTo(relative_to, obj) |
41 self._identity = identity or type(self).__name__ | 107 self._identity = identity or type(self).__name__ |
42 self._path_stats = {} | 108 self._path_values = _List(obj) |
43 self._global_stat = 0 | 109 self._stat_tracker = _StatTracker() |
44 | 110 |
45 # | 111 # |
46 # FileSystem implementation. | 112 # FileSystem implementation. |
47 # | 113 # |
48 | 114 |
49 def Read(self, paths): | 115 def Read(self, paths): |
50 test_fs = self | 116 for path in paths: |
51 class Delegate(object): | 117 if path not in self._path_values: |
52 def Get(self): | 118 return FileNotFoundError.RaiseInFuture(path) |
53 return dict((path, test_fs._ResolvePath(path)) for path in paths) | 119 return Future(value=dict((k, v) for k, v in self._path_values.iteritems() |
54 return Future(delegate=Delegate()) | 120 if k in paths)) |
55 | 121 |
56 def Refresh(self): | 122 def Refresh(self): |
57 return Future(value=()) | 123 return Future(value=()) |
58 | 124 |
59 def _ResolvePath(self, path): | |
60 def Resolve(parts): | |
61 '''Resolves |parts| of a path info |self._obj|. | |
62 ''' | |
63 result = self._obj.get(parts[0]) | |
64 for part in parts[1:]: | |
65 if not isinstance(result, dict): | |
66 raise FileNotFoundError( | |
67 '%s at %s did not resolve to a dict, instead %s' % | |
68 (path, part, result)) | |
69 result = result.get(part) | |
70 return result | |
71 | |
72 def GetPaths(obj): | |
73 '''Lists the paths within |obj|; this is basially keys() but with | |
74 directory paths (i.e. dicts) with a trailing /. | |
75 ''' | |
76 def ToPath(k, v): | |
77 if isinstance(v, basestring): | |
78 return k | |
79 if isinstance(v, dict): | |
80 return '%s/' % k | |
81 raise ValueError('Cannot convert type %s to path', type(v)) | |
82 return [ToPath(k, v) for k, v in obj.items()] | |
83 | |
84 path = path.lstrip('/') | |
85 | |
86 if path == '': | |
87 return GetPaths(self._obj) | |
88 | |
89 parts = path.split('/') | |
90 if parts[-1] != '': | |
91 file_contents = Resolve(parts) | |
92 if not isinstance(file_contents, basestring): | |
93 raise FileNotFoundError( | |
94 '%s (%s) did not resolve to a string, instead %s' % | |
95 (path, parts, file_contents)) | |
96 return file_contents | |
97 | |
98 dir_contents = Resolve(parts[:-1]) | |
99 if not isinstance(dir_contents, dict): | |
100 raise FileNotFoundError( | |
101 '%s (%s) did not resolve to a dict, instead %s' % | |
102 (path, parts, dir_contents)) | |
103 | |
104 return GetPaths(dir_contents) | |
105 | |
106 def Stat(self, path): | 125 def Stat(self, path): |
107 read_result = self.Read([path]).Get().get(path) | 126 read_result = self.ReadSingle(path).Get() |
108 stat_result = StatInfo(self._SinglePathStat(path)) | 127 stat_result = StatInfo(str(self._stat_tracker.GetVersion(path))) |
109 if isinstance(read_result, list): | 128 if isinstance(read_result, list): |
110 stat_result.child_versions = dict( | 129 stat_result.child_versions = dict( |
111 (file_result, self._SinglePathStat('%s%s' % (path, file_result))) | 130 (file_result, |
| 131 str(self._stat_tracker.GetVersion('%s%s' % (path, file_result)))) |
112 for file_result in read_result) | 132 for file_result in read_result) |
113 return stat_result | 133 return stat_result |
114 | 134 |
115 def _SinglePathStat(self, path): | |
116 return str(self._global_stat + self._path_stats.get(path, 0)) | |
117 | |
118 # | 135 # |
119 # Testing methods. | 136 # Testing methods. |
120 # | 137 # |
121 | 138 |
122 def IncrementStat(self, path=None, by=1): | 139 def IncrementStat(self, path=None, by=1): |
123 if path is not None: | 140 self._stat_tracker.Increment(path, by=by) |
124 self._path_stats[path] = self._path_stats.get(path, 0) + by | |
125 else: | |
126 self._global_stat += by | |
127 | 141 |
128 def GetIdentity(self): | 142 def GetIdentity(self): |
129 return self._identity | 143 return self._identity |
OLD | NEW |