| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Wrapper class to store size related information of test results. |
| """ |
| |
| import contextlib |
| import json |
| import os |
| |
| import result_info_lib |
| import utils_lib |
| |
| |
| class ResultInfoError(Exception): |
| """Exception to raise when error occurs in ResultInfo collection.""" |
| |
| |
| class ResultInfo(dict): |
| """A wrapper class to store result file information. |
| |
| Details of a result include: |
| original_size: Original size in bytes of the result, before throttling. |
| trimmed_size: Size in bytes after the result is throttled. |
| collected_size: Size in bytes of the results collected from the dut. |
| files: A list of ResultInfo for the files and sub-directories of the result. |
| |
| The class contains the size information of a result file/directory, and the |
| information can be merged if a file was collected multiple times during |
| the test. |
| For example, `messages` of size 100 bytes was collected before the test |
| starts, ResultInfo for this file shall be: |
| {'messages': {'/S': 100}} |
| Later in the test, the file was collected again when it's size becomes 200 |
| bytes, the new ResultInfo will be: |
| {'messages': {'/S': 200}} |
| |
| Not that the result infos collected from the dut don't have collected_size |
| (/C) set. That's because the collected size in such case is equal to the |
| trimmed_size (/T). If the reuslt is not trimmed and /T is not set, the |
| value of collected_size can fall back to original_size. The design is to not |
| to inject duplicated information in the summary json file, thus reduce the |
| size of data needs to be transfered from the dut. |
| |
| At the end of the test, the file is considered too big, and trimmed down to |
| 150 bytes, thus the final ResultInfo of the file becomes: |
| {'messages': {# The original size is 200 bytes |
| '/S': 200, |
| # The total collected size is 300(100+200} bytes |
| '/C': 300, |
| # The trimmed size is the final size on disk |
| '/T': 150} |
| From this example, the original size tells us how large the file was. |
| The collected size tells us how much data was transfered from dut to drone |
| to get this file. And the trimmed size shows the final size of the file when |
| the test is finished and the results are throttled again on the server side. |
| |
| The class is a wrapper of dictionary. The properties are all keyvals in a |
| dictionary. For example, an instance of ResultInfo can have following |
| dictionary value: |
| {'debug': { |
| # Original size of the debug folder is 1000 bytes. |
| '/S': 1000, |
| # The debug folder was throttled and the size is reduced to 500 |
| # bytes. |
| '/T': 500, |
| # collected_size ('/C') can be ignored, its value falls back to |
| # trimmed_size ('/T'). If trimmed_size is not set, its value falls |
| # back to original_size ('S') |
| |
| # Sub-files and sub-directories are included in a list of '/D''s |
| # value. |
| # In this example, debug folder has a file `file1`, whose original |
| # size is 1000 bytes, which is trimmed down to 500 bytes. |
| '/D': [ |
| {'file1': { |
| '/S': 1000, |
| '/T': 500, |
| } |
| } |
| ] |
| } |
| } |
| """ |
| |
| def __init__(self, parent_dir, name=None, parent_result_info=None, |
| original_info=None): |
| """Initialize a collection of size information for a given result path. |
| |
| A ResultInfo object can be initialized in two ways: |
| 1. Create from a physical file, which reads the size from the file. |
| In this case, `name` value should be given, and `original_info` |
| should not be set. |
| 2. Create from previously collected information, i.e., a dictionary |
| deserialized from persisted json file. In this case, `original_info` |
| should be given, and `name` should not be set. |
| |
| @param parent_dir: Path to the parent directory. |
| @param name: Name of the result file or directory. |
| @param parent_result_info: A ResultInfo object for the parent directory. |
| @param original_info: A dictionary of the result's size information. |
| This is retrieved from the previously serialized json string. |
| For example: {'file_name': |
| {'/S': 100, '/T': 50} |
| } |
| which means a file's original size is 100 bytes, and trimmed |
| down to 50 bytes. This argument is used when the object is |
| restored from a json string. |
| """ |
| super(ResultInfo, self).__init__() |
| |
| if name is not None and original_info is not None: |
| raise ResultInfoError( |
| 'Only one of parameter `name` and `original_info` can be ' |
| 'set.') |
| |
| # _initialized is a flag to indicating the object is in constructor. |
| # It can be used to block any size update to make restoring from json |
| # string faster. For example, if file_details has sub-directories, |
| # all sub-directories will be added to this class recursively, blocking |
| # the size updates can reduce unnecessary calculations. |
| self._initialized = False |
| self._parent_result_info = parent_result_info |
| |
| if original_info is None: |
| self._init_from_file(parent_dir, name) |
| else: |
| self._init_with_original_info(parent_dir, original_info) |
| |
| # Size of bytes collected in an overwritten or removed directory. |
| self._previous_collected_size = 0 |
| self._initialized = True |
| |
| def _init_from_file(self, parent_dir, name): |
| """Initialize with the physical file. |
| |
| @param parent_dir: Path to the parent directory. |
| @param name: Name of the result file or directory. |
| """ |
| assert name != None |
| self._name = name |
| |
| # Dictionary to store details of the given path is set to a keyval of |
| # the wrapper class. Save the dictionary to an attribute for faster |
| # access. |
| self._details = {} |
| self[self.name] = self._details |
| |
| # rstrip is to remove / when name is ROOT_DIR (''). |
| self._path = os.path.join(parent_dir, self.name).rstrip(os.sep) |
| self._is_dir = os.path.isdir(self._path) |
| |
| if self.is_dir: |
| # The value of key utils_lib.DIRS is a list of ResultInfo objects. |
| self.details[utils_lib.DIRS] = [] |
| |
| # Set original size to be the physical size if file details are not |
| # given and the path is for a file. |
| if self.is_dir: |
| # Set directory size to 0, it will be updated later after its |
| # sub-directories are added. |
| self.original_size = 0 |
| else: |
| self.original_size = self.size |
| |
| def _init_with_original_info(self, parent_dir, original_info): |
| """Initialize with pre-collected information. |
| |
| @param parent_dir: Path to the parent directory. |
| @param original_info: A dictionary of the result's size information. |
| This is retrieved from the previously serialized json string. |
| For example: {'file_name': |
| {'/S': 100, '/T': 50} |
| } |
| which means a file's original size is 100 bytes, and trimmed |
| down to 50 bytes. This argument is used when the object is |
| restored from a json string. |
| """ |
| assert original_info |
| # The result information dictionary has only 1 key, which is the file or |
| # directory name. |
| self._name = original_info.keys()[0] |
| |
| # Dictionary to store details of the given path is set to a keyval of |
| # the wrapper class. Save the dictionary to an attribute for faster |
| # access. |
| self._details = {} |
| self[self.name] = self._details |
| |
| # rstrip is to remove / when name is ROOT_DIR (''). |
| self._path = os.path.join(parent_dir, self.name).rstrip(os.sep) |
| |
| self._is_dir = utils_lib.DIRS in original_info[self.name] |
| |
| if self.is_dir: |
| # The value of key utils_lib.DIRS is a list of ResultInfo objects. |
| self.details[utils_lib.DIRS] = [] |
| |
| # This is restoring ResultInfo from a json string. |
| self.original_size = original_info[self.name][ |
| utils_lib.ORIGINAL_SIZE_BYTES] |
| if utils_lib.TRIMMED_SIZE_BYTES in original_info[self.name]: |
| self.trimmed_size = original_info[self.name][ |
| utils_lib.TRIMMED_SIZE_BYTES] |
| if self.is_dir: |
| dirs = original_info[self.name][utils_lib.DIRS] |
| # TODO: Remove this conversion after R62 is in stable channel. |
| if isinstance(dirs, dict): |
| # The summary is generated from older format which stores sub- |
| # directories in a dictionary, rather than a list. Convert the |
| # data in old format to a list of dictionary. |
| dirs = [{dir_name: dirs[dir_name]} for dir_name in dirs] |
| for sub_file in dirs: |
| self.add_file(None, sub_file) |
| |
| @contextlib.contextmanager |
| def disable_updating_parent_size_info(self): |
| """Disable recursive calls to update parent result_info's sizes. |
| |
| This context manager allows removing sub-directories to run faster |
| without triggering recursive calls to update parent result_info's sizes. |
| """ |
| old_value = self._initialized |
| self._initialized = False |
| try: |
| yield |
| finally: |
| self._initialized = old_value |
| |
| def update_dir_original_size(self): |
| """Update all directories' original size information. |
| """ |
| for f in [f for f in self.files if f.is_dir]: |
| f.update_dir_original_size() |
| self.update_original_size(skip_parent_update=True) |
| |
| @staticmethod |
| def build_from_path(parent_dir, |
| name=utils_lib.ROOT_DIR, |
| parent_result_info=None, top_dir=None, |
| all_dirs=None): |
| """Get the ResultInfo for the given path. |
| |
| @param parent_dir: The parent directory of the given file. |
| @param name: Name of the result file or directory. |
| @param parent_result_info: A ResultInfo instance for the parent |
| directory. |
| @param top_dir: The top directory to collect ResultInfo. This is to |
| check if a directory is a subdir of the original directory to |
| collect summary. |
| @param all_dirs: A set of paths that have been collected. This is to |
| prevent infinite recursive call caused by symlink. |
| |
| @return: A ResultInfo instance containing the directory summary. |
| """ |
| is_top_level = top_dir is None |
| top_dir = top_dir or parent_dir |
| all_dirs = all_dirs or set() |
| |
| # If the given parent_dir is a file and name is ROOT_DIR, that means |
| # the ResultInfo is for a single file with root directory of the default |
| # ROOT_DIR. |
| if not os.path.isdir(parent_dir) and name == utils_lib.ROOT_DIR: |
| root_dir = os.path.dirname(parent_dir) |
| dir_info = ResultInfo(parent_dir=root_dir, |
| name=utils_lib.ROOT_DIR) |
| dir_info.add_file(os.path.basename(parent_dir)) |
| return dir_info |
| |
| dir_info = ResultInfo(parent_dir=parent_dir, |
| name=name, |
| parent_result_info=parent_result_info) |
| |
| path = os.path.join(parent_dir, name) |
| if os.path.isdir(path): |
| real_path = os.path.realpath(path) |
| # The assumption here is that results are copied back to drone by |
| # copying the symlink, not the content, which is true with currently |
| # used rsync in cros_host.get_file call. |
| # Skip scanning the child folders if any of following condition is |
| # true: |
| # 1. The directory is a symlink and link to a folder under `top_dir` |
| # 2. The directory was scanned already. |
| if ((os.path.islink(path) and real_path.startswith(top_dir)) or |
| real_path in all_dirs): |
| return dir_info |
| all_dirs.add(real_path) |
| for f in sorted(os.listdir(path)): |
| dir_info.files.append(ResultInfo.build_from_path( |
| parent_dir=path, |
| name=f, |
| parent_result_info=dir_info, |
| top_dir=top_dir, |
| all_dirs=all_dirs)) |
| |
| # Update all directory's original size at the end of the tree building. |
| if is_top_level: |
| dir_info.update_dir_original_size() |
| |
| return dir_info |
| |
| @property |
| def details(self): |
| """Get the details of the result. |
| |
| @return: A dictionary of size and sub-directory information. |
| """ |
| return self._details |
| |
| @property |
| def is_dir(self): |
| """Get if the result is a directory. |
| """ |
| return self._is_dir |
| |
| @property |
| def name(self): |
| """Name of the result. |
| """ |
| return self._name |
| |
| @property |
| def path(self): |
| """Full path to the result. |
| """ |
| return self._path |
| |
| @property |
| def files(self): |
| """All files or sub-directories of the result. |
| |
| @return: A list of ResultInfo objects. |
| @raise ResultInfoError: If the result is not a directory. |
| """ |
| if not self.is_dir: |
| raise ResultInfoError('%s is not a directory.' % self.path) |
| return self.details[utils_lib.DIRS] |
| |
| @property |
| def size(self): |
| """Physical size in bytes for the result file. |
| |
| @raise ResultInfoError: If the result is a directory. |
| """ |
| if self.is_dir: |
| raise ResultInfoError( |
| '`size` property does not support directory. Try to use ' |
| '`original_size` property instead.') |
| return result_info_lib.get_file_size(self._path) |
| |
| @property |
| def original_size(self): |
| """The original size in bytes of the result before it's throttled. |
| """ |
| return self.details[utils_lib.ORIGINAL_SIZE_BYTES] |
| |
| @original_size.setter |
| def original_size(self, value): |
| """Set the original size in bytes of the result. |
| |
| @param value: The original size in bytes of the result. |
| """ |
| self.details[utils_lib.ORIGINAL_SIZE_BYTES] = value |
| # Update the size of parent result infos if the object is already |
| # initialized. |
| if self._initialized and self._parent_result_info is not None: |
| self._parent_result_info.update_original_size() |
| |
| @property |
| def trimmed_size(self): |
| """The size in bytes of the result after it's throttled. |
| """ |
| return self.details.get(utils_lib.TRIMMED_SIZE_BYTES, |
| self.original_size) |
| |
| @trimmed_size.setter |
| def trimmed_size(self, value): |
| """Set the trimmed size in bytes of the result. |
| |
| @param value: The trimmed size in bytes of the result. |
| """ |
| self.details[utils_lib.TRIMMED_SIZE_BYTES] = value |
| # Update the size of parent result infos if the object is already |
| # initialized. |
| if self._initialized and self._parent_result_info is not None: |
| self._parent_result_info.update_trimmed_size() |
| |
| @property |
| def collected_size(self): |
| """The collected size in bytes of the result. |
| |
| The file is throttled on the dut, so the number of bytes collected from |
| dut is default to the trimmed_size. If a file is modified between |
| multiple result collections and is collected multiple times during the |
| test run, the collected_size will be the sum of the multiple |
| collections. Therefore, its value will be greater than the trimmed_size |
| of the last copy. |
| """ |
| return self.details.get(utils_lib.COLLECTED_SIZE_BYTES, |
| self.trimmed_size) |
| |
| @collected_size.setter |
| def collected_size(self, value): |
| """Set the collected size in bytes of the result. |
| |
| @param value: The collected size in bytes of the result. |
| """ |
| self.details[utils_lib.COLLECTED_SIZE_BYTES] = value |
| # Update the size of parent result infos if the object is already |
| # initialized. |
| if self._initialized and self._parent_result_info is not None: |
| self._parent_result_info.update_collected_size() |
| |
| @property |
| def is_collected_size_recorded(self): |
| """Flag to indicate if the result has collected size set. |
| |
| This flag is used to avoid unnecessary entry in result details, as the |
| default value of collected size is the trimmed size. Removing the |
| redundant information helps to reduce the size of the json file. |
| """ |
| return utils_lib.COLLECTED_SIZE_BYTES in self.details |
| |
| @property |
| def parent_result_info(self): |
| """The result info of the parent directory. |
| """ |
| return self._parent_result_info |
| |
| def add_file(self, name, original_info=None): |
| """Add a file to the result. |
| |
| @param name: Name of the file. |
| @param original_info: A dictionary of the file's size and sub-directory |
| information. |
| """ |
| self.details[utils_lib.DIRS].append( |
| ResultInfo(parent_dir=self._path, |
| name=name, |
| parent_result_info=self, |
| original_info=original_info)) |
| # After a new ResultInfo is added, update the sizes if the object is |
| # already initialized. |
| if self._initialized: |
| self.update_sizes() |
| |
| def remove_file(self, name): |
| """Remove a file with the given name from the result. |
| |
| @param name: Name of the file to be removed. |
| """ |
| self.files.remove(self.get_file(name)) |
| # After a new ResultInfo is removed, update the sizes if the object is |
| # already initialized. |
| if self._initialized: |
| self.update_sizes() |
| |
| def get_file_names(self): |
| """Get a set of all the files under the result. |
| """ |
| return set([f.keys()[0] for f in self.files]) |
| |
| def get_file(self, name): |
| """Get a file with the given name under the result. |
| |
| @param name: Name of the file. |
| @return: A ResultInfo object of the file. |
| @raise ResultInfoError: If the result is not a directory, or the file |
| with the given name is not found. |
| """ |
| if not self.is_dir: |
| raise ResultInfoError('%s is not a directory. Can\'t locate file ' |
| '%s' % (self.path, name)) |
| for file_info in self.files: |
| if file_info.name == name: |
| return file_info |
| raise ResultInfoError('Can\'t locate file %s in directory %s' % |
| (name, self.path)) |
| |
| def convert_to_dir(self): |
| """Convert the result file to a directory. |
| |
| This happens when a result file was overwritten by a directory. The |
| conversion will reset the details of this result to be a directory, |
| and save the collected_size to attribute `_previous_collected_size`, |
| so it can be counted when merging multiple result infos. |
| |
| @raise ResultInfoError: If the result is already a directory. |
| """ |
| if self.is_dir: |
| raise ResultInfoError('%s is already a directory.' % self.path) |
| # The size that's collected before the file was replaced as a directory. |
| collected_size = self.collected_size |
| self._is_dir = True |
| self.details[utils_lib.DIRS] = [] |
| self.original_size = 0 |
| self.trimmed_size = 0 |
| self._previous_collected_size = collected_size |
| self.collected_size = collected_size |
| |
| def update_original_size(self, skip_parent_update=False): |
| """Update the original size of the result and trigger its parent to |
| update. |
| |
| @param skip_parent_update: True to skip updating parent directory's |
| original size. Default is set to False. |
| """ |
| if self.is_dir: |
| self.original_size = sum([ |
| f.original_size for f in self.files]) |
| elif self.original_size is None: |
| # Only set original_size if it's not initialized yet. |
| self.orginal_size = self.size |
| |
| # Update the size of parent result infos. |
| if not skip_parent_update and self._parent_result_info is not None: |
| self._parent_result_info.update_original_size() |
| |
| def update_trimmed_size(self): |
| """Update the trimmed size of the result and trigger its parent to |
| update. |
| """ |
| if self.is_dir: |
| new_trimmed_size = sum([f.trimmed_size for f in self.files]) |
| else: |
| new_trimmed_size = self.size |
| |
| # Only set trimmed_size if the value is changed or different from the |
| # original size. |
| if (new_trimmed_size != self.original_size or |
| new_trimmed_size != self.trimmed_size): |
| self.trimmed_size = new_trimmed_size |
| |
| # Update the size of parent result infos. |
| if self._parent_result_info is not None: |
| self._parent_result_info.update_trimmed_size() |
| |
| def update_collected_size(self): |
| """Update the collected size of the result and trigger its parent to |
| update. |
| """ |
| if self.is_dir: |
| new_collected_size = ( |
| self._previous_collected_size + |
| sum([f.collected_size for f in self.files])) |
| else: |
| new_collected_size = self.size |
| |
| # Only set collected_size if the value is changed or different from the |
| # trimmed size or existing collected size. |
| if (new_collected_size != self.trimmed_size or |
| new_collected_size != self.collected_size): |
| self.collected_size = new_collected_size |
| |
| # Update the size of parent result infos. |
| if self._parent_result_info is not None: |
| self._parent_result_info.update_collected_size() |
| |
| def update_sizes(self): |
| """Update all sizes information of the result. |
| """ |
| self.update_original_size() |
| self.update_trimmed_size() |
| self.update_collected_size() |
| |
| def set_parent_result_info(self, parent_result_info, update_sizes=True): |
| """Set the parent result info. |
| |
| It's used when a ResultInfo object is moved to a different file |
| structure. |
| |
| @param parent_result_info: A ResultInfo object for the parent directory. |
| @param update_sizes: True to update the parent's size information. Set |
| it to False to delay the update for better performance. |
| """ |
| self._parent_result_info = parent_result_info |
| # As the parent reference changed, update all sizes of the parent. |
| if parent_result_info and update_sizes: |
| self._parent_result_info.update_sizes() |
| |
| def merge(self, new_info, is_final=False): |
| """Merge a ResultInfo instance to the current one. |
| |
| Update the old directory's ResultInfo with the new one. Also calculate |
| the total size of results collected from the client side based on the |
| difference between the two ResultInfo. |
| |
| When merging with newer collected results, any results not existing in |
| the new ResultInfo or files with size different from the newer files |
| collected are considered as extra results collected or overwritten by |
| the new results. |
| Therefore, the size of the collected result should include such files, |
| and the collected size can be larger than trimmed size. |
| As an example: |
| current: {'file1': {TRIMMED_SIZE_BYTES: 1024, |
| ORIGINAL_SIZE_BYTES: 1024, |
| COLLECTED_SIZE_BYTES: 1024}} |
| This means a result `file1` of original size 1KB was collected with size |
| of 1KB byte. |
| new_info: {'file1': {TRIMMED_SIZE_BYTES: 1024, |
| ORIGINAL_SIZE_BYTES: 2048, |
| COLLECTED_SIZE_BYTES: 1024}} |
| This means a result `file1` of 2KB was trimmed down to 1KB and was |
| collected with size of 1KB byte. |
| Note that the second result collection has an updated result `file1` |
| (because of the different ORIGINAL_SIZE_BYTES), and it needs to be |
| rsync-ed to the drone. Therefore, the merged ResultInfo will be: |
| {'file1': {TRIMMED_SIZE_BYTES: 1024, |
| ORIGINAL_SIZE_BYTES: 2048, |
| COLLECTED_SIZE_BYTES: 2048}} |
| Note that: |
| * TRIMMED_SIZE_BYTES is still at 1KB, which reflects the actual size of |
| the file be collected. |
| * ORIGINAL_SIZE_BYTES is updated to 2KB, which is the size of the file |
| in the new result `file1`. |
| * COLLECTED_SIZE_BYTES is 2KB because rsync will copy `file1` twice as |
| it's changed. |
| |
| The only exception is that the new ResultInfo's ORIGINAL_SIZE_BYTES is |
| the same as the current ResultInfo's TRIMMED_SIZE_BYTES. That means the |
| file was trimmed in the current ResultInfo and the new ResultInfo is |
| collecting the trimmed file. Therefore, the merged summary will keep the |
| data in the current ResultInfo. |
| |
| @param new_info: New ResultInfo to be merged into the current one. |
| @param is_final: True if new_info is built from the final result folder. |
| Default is set to False. |
| """ |
| new_files = new_info.get_file_names() |
| old_files = self.get_file_names() |
| # A flag to indicate if the sizes need to be updated. It's required when |
| # child result_info is added to `self`. |
| update_sizes_pending = False |
| for name in new_files: |
| new_file = new_info.get_file(name) |
| if not name in old_files: |
| # A file/dir exists in new client dir, but not in the old one, |
| # which means that the file or a directory is newly collected. |
| self.files.append(new_file) |
| # Once parent_result_info is changed, new_file object will no |
| # longer associated with `new_info` object. |
| new_file.set_parent_result_info(self, update_sizes=False) |
| update_sizes_pending = True |
| elif new_file.is_dir: |
| # `name` is a directory in the new ResultInfo, try to merge it |
| # with the current ResultInfo. |
| old_file = self.get_file(name) |
| |
| if not old_file.is_dir: |
| # If `name` is a file in the current ResultInfo but a |
| # directory in new ResultInfo, the file in the current |
| # ResultInfo will be overwritten by the new directory by |
| # rsync. Therefore, force it to be an empty directory in |
| # the current ResultInfo, so that the new directory can be |
| # merged. |
| old_file.convert_to_dir() |
| |
| old_file.merge(new_file, is_final) |
| else: |
| old_file = self.get_file(name) |
| |
| # If `name` is a directory in the current ResultInfo, but a file |
| # in the new ResultInfo, rsync will fail to copy the file as it |
| # can't overwrite an directory. Therefore, skip the merge. |
| if old_file.is_dir: |
| continue |
| |
| new_size = new_file.original_size |
| old_size = old_file.original_size |
| new_trimmed_size = new_file.trimmed_size |
| old_trimmed_size = old_file.trimmed_size |
| |
| # Keep current information if the sizes are not changed. |
| if (new_size == old_size and |
| new_trimmed_size == old_trimmed_size): |
| continue |
| |
| # Keep current information if the newer size is the same as the |
| # current trimmed size, and the file is not trimmed in new |
| # ResultInfo. That means the file was trimmed earlier and stays |
| # the same when collecting the information again. |
| if (new_size == old_trimmed_size and |
| new_size == new_trimmed_size): |
| continue |
| |
| # If the file is merged from the final result folder to an older |
| # ResultInfo, it's not considered to be trimmed if the size is |
| # not changed. The reason is that the file on the server side |
| # does not have the info of its original size. |
| if is_final and new_trimmed_size == old_trimmed_size: |
| continue |
| |
| # `name` is a file, and both the original_size and trimmed_size |
| # are changed, that means the file is overwritten, so increment |
| # the collected_size. |
| # Before trimming is implemented, collected_size is the |
| # value of original_size. |
| new_collected_size = new_file.collected_size |
| old_collected_size = old_file.collected_size |
| |
| old_file.collected_size = ( |
| new_collected_size + old_collected_size) |
| # Only set trimmed_size if one of the following two conditions |
| # are true: |
| # 1. In the new summary the file's trimmed size is different |
| # from the original size, which means the file was trimmed |
| # in the new summary. |
| # 2. The original size in the new summary equals the trimmed |
| # size in the old summary, which means the file was trimmed |
| # again in the new summary. |
| if (new_size == old_trimmed_size or |
| new_size != new_trimmed_size): |
| old_file.trimmed_size = new_file.trimmed_size |
| old_file.original_size = new_size |
| |
| if update_sizes_pending: |
| self.update_sizes() |
| |
| |
| # An empty directory, used to compare with a ResultInfo. |
| EMPTY = ResultInfo(parent_dir='', |
| original_info={'': {utils_lib.ORIGINAL_SIZE_BYTES: 0, |
| utils_lib.DIRS: []}}) |
| |
| |
| def save_summary(summary, json_file): |
| """Save the given directory summary to a file. |
| |
| @param summary: A ResultInfo object for a result directory. |
| @param json_file: Path to a json file to save to. |
| """ |
| with open(json_file, 'w') as f: |
| json.dump(summary, f) |
| |
| |
| def load_summary_json_file(json_file): |
| """Load result info from the given json_file. |
| |
| @param json_file: Path to a json file containing a directory summary. |
| @return: A ResultInfo object containing the directory summary. |
| """ |
| with open(json_file, 'r') as f: |
| summary = json.load(f) |
| |
| # Convert summary to ResultInfo objects |
| result_dir = os.path.dirname(json_file) |
| return ResultInfo(parent_dir=result_dir, original_info=summary) |