| 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 |