• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Wrapper class to store size related information of test results.
7"""
8
9import contextlib
10import json
11import os
12
13try:
14    from autotest_lib.client.bin.result_tools import result_info_lib
15    from autotest_lib.client.bin.result_tools import utils_lib
16except ImportError:
17    import result_info_lib
18    import utils_lib
19
20
21class ResultInfoError(Exception):
22    """Exception to raise when error occurs in ResultInfo collection."""
23
24
25class ResultInfo(dict):
26    """A wrapper class to store result file information.
27
28    Details of a result include:
29    original_size: Original size in bytes of the result, before throttling.
30    trimmed_size: Size in bytes after the result is throttled.
31    collected_size: Size in bytes of the results collected from the dut.
32    files: A list of ResultInfo for the files and sub-directories of the result.
33
34    The class contains the size information of a result file/directory, and the
35    information can be merged if a file was collected multiple times during
36    the test.
37    For example, `messages` of size 100 bytes was collected before the test
38    starts, ResultInfo for this file shall be:
39        {'messages': {'/S': 100}}
40    Later in the test, the file was collected again when it's size becomes 200
41    bytes, the new ResultInfo will be:
42        {'messages': {'/S': 200}}
43
44    Not that the result infos collected from the dut don't have collected_size
45    (/C) set. That's because the collected size in such case is equal to the
46    trimmed_size (/T). If the reuslt is not trimmed and /T is not set, the
47    value of collected_size can fall back to original_size. The design is to not
48    to inject duplicated information in the summary json file, thus reduce the
49    size of data needs to be transfered from the dut.
50
51    At the end of the test, the file is considered too big, and trimmed down to
52    150 bytes, thus the final ResultInfo of the file becomes:
53        {'messages': {# The original size is 200 bytes
54                      '/S': 200,
55                      # The total collected size is 300(100+200} bytes
56                      '/C': 300,
57                      # The trimmed size is the final size on disk
58                      '/T': 150}
59    From this example, the original size tells us how large the file was.
60    The collected size tells us how much data was transfered from dut to drone
61    to get this file. And the trimmed size shows the final size of the file when
62    the test is finished and the results are throttled again on the server side.
63
64    The class is a wrapper of dictionary. The properties are all keyvals in a
65    dictionary. For example, an instance of ResultInfo can have following
66    dictionary value:
67    {'debug': {
68            # Original size of the debug folder is 1000 bytes.
69            '/S': 1000,
70            # The debug folder was throttled and the size is reduced to 500
71            # bytes.
72            '/T': 500,
73            # collected_size ('/C') can be ignored, its value falls back to
74            # trimmed_size ('/T'). If trimmed_size is not set, its value falls
75            # back to original_size ('S')
76
77            # Sub-files and sub-directories are included in a list of '/D''s
78            # value.
79            # In this example, debug folder has a file `file1`, whose original
80            # size is 1000 bytes, which is trimmed down to 500 bytes.
81            '/D': [
82                    {'file1': {
83                            '/S': 1000,
84                            '/T': 500,
85                        }
86                    }
87                ]
88        }
89    }
90    """
91
92    def __init__(self, parent_dir, name=None, parent_result_info=None,
93                 original_info=None):
94        """Initialize a collection of size information for a given result path.
95
96        A ResultInfo object can be initialized in two ways:
97        1. Create from a physical file, which reads the size from the file.
98           In this case, `name` value should be given, and `original_info`
99           should not be set.
100        2. Create from previously collected information, i.e., a dictionary
101           deserialized from persisted json file. In this case, `original_info`
102           should be given, and `name` should not be set.
103
104        @param parent_dir: Path to the parent directory.
105        @param name: Name of the result file or directory.
106        @param parent_result_info: A ResultInfo object for the parent directory.
107        @param original_info: A dictionary of the result's size information.
108                This is retrieved from the previously serialized json string.
109                For example: {'file_name':
110                            {'/S': 100, '/T': 50}
111                         }
112                which means a file's original size is 100 bytes, and trimmed
113                down to 50 bytes. This argument is used when the object is
114                restored from a json string.
115        """
116        super(ResultInfo, self).__init__()
117
118        if name is not None and original_info is not None:
119            raise ResultInfoError(
120                    'Only one of parameter `name` and `original_info` can be '
121                    'set.')
122
123        # _initialized is a flag to indicating the object is in constructor.
124        # It can be used to block any size update to make restoring from json
125        # string faster. For example, if file_details has sub-directories,
126        # all sub-directories will be added to this class recursively, blocking
127        # the size updates can reduce unnecessary calculations.
128        self._initialized = False
129        self._parent_result_info = parent_result_info
130
131        if original_info is None:
132            self._init_from_file(parent_dir, name)
133        else:
134            self._init_with_original_info(parent_dir, original_info)
135
136        # Size of bytes collected in an overwritten or removed directory.
137        self._previous_collected_size = 0
138        self._initialized = True
139
140    def _init_from_file(self, parent_dir, name):
141        """Initialize with the physical file.
142
143        @param parent_dir: Path to the parent directory.
144        @param name: Name of the result file or directory.
145        """
146        assert name != None
147        self._name = name
148
149        # Dictionary to store details of the given path is set to a keyval of
150        # the wrapper class. Save the dictionary to an attribute for faster
151        # access.
152        self._details = {}
153        self[self.name] = self._details
154
155        # rstrip is to remove / when name is ROOT_DIR ('').
156        self._path = os.path.join(parent_dir, self.name).rstrip(os.sep)
157        self._is_dir = os.path.isdir(self._path)
158
159        if self.is_dir:
160            # The value of key utils_lib.DIRS is a list of ResultInfo objects.
161            self.details[utils_lib.DIRS] = []
162
163        # Set original size to be the physical size if file details are not
164        # given and the path is for a file.
165        if self.is_dir:
166            # Set directory size to 0, it will be updated later after its
167            # sub-directories are added.
168            self.original_size = 0
169        else:
170            self.original_size = self.size
171
172    def _init_with_original_info(self, parent_dir, original_info):
173        """Initialize with pre-collected information.
174
175        @param parent_dir: Path to the parent directory.
176        @param original_info: A dictionary of the result's size information.
177                This is retrieved from the previously serialized json string.
178                For example: {'file_name':
179                            {'/S': 100, '/T': 50}
180                         }
181                which means a file's original size is 100 bytes, and trimmed
182                down to 50 bytes. This argument is used when the object is
183                restored from a json string.
184        """
185        assert original_info
186        # The result information dictionary has only 1 key, which is the file or
187        # directory name.
188        self._name = list(original_info.keys())[0]
189
190        # Dictionary to store details of the given path is set to a keyval of
191        # the wrapper class. Save the dictionary to an attribute for faster
192        # access.
193        self._details = {}
194        self[self.name] = self._details
195
196        # rstrip is to remove / when name is ROOT_DIR ('').
197        self._path = os.path.join(parent_dir, self.name).rstrip(os.sep)
198
199        self._is_dir = utils_lib.DIRS in original_info[self.name]
200
201        if self.is_dir:
202            # The value of key utils_lib.DIRS is a list of ResultInfo objects.
203            self.details[utils_lib.DIRS] = []
204
205        # This is restoring ResultInfo from a json string.
206        self.original_size = original_info[self.name][
207                utils_lib.ORIGINAL_SIZE_BYTES]
208        if utils_lib.TRIMMED_SIZE_BYTES in original_info[self.name]:
209            self.trimmed_size = original_info[self.name][
210                    utils_lib.TRIMMED_SIZE_BYTES]
211        if self.is_dir:
212            dirs = original_info[self.name][utils_lib.DIRS]
213            # TODO: Remove this conversion after R62 is in stable channel.
214            if isinstance(dirs, dict):
215                # The summary is generated from older format which stores sub-
216                # directories in a dictionary, rather than a list. Convert the
217                # data in old format to a list of dictionary.
218                dirs = [{dir_name: dirs[dir_name]} for dir_name in dirs]
219            for sub_file in dirs:
220                self.add_file(None, sub_file)
221
222    @contextlib.contextmanager
223    def disable_updating_parent_size_info(self):
224        """Disable recursive calls to update parent result_info's sizes.
225
226        This context manager allows removing sub-directories to run faster
227        without triggering recursive calls to update parent result_info's sizes.
228        """
229        old_value = self._initialized
230        self._initialized = False
231        try:
232            yield
233        finally:
234            self._initialized = old_value
235
236    def update_dir_original_size(self):
237        """Update all directories' original size information.
238        """
239        for f in [f for f in self.files if f.is_dir]:
240            f.update_dir_original_size()
241        self.update_original_size(skip_parent_update=True)
242
243    @staticmethod
244    def build_from_path(parent_dir,
245                        name=utils_lib.ROOT_DIR,
246                        parent_result_info=None, top_dir=None,
247                        all_dirs=None):
248        """Get the ResultInfo for the given path.
249
250        @param parent_dir: The parent directory of the given file.
251        @param name: Name of the result file or directory.
252        @param parent_result_info: A ResultInfo instance for the parent
253                directory.
254        @param top_dir: The top directory to collect ResultInfo. This is to
255                check if a directory is a subdir of the original directory to
256                collect summary.
257        @param all_dirs: A set of paths that have been collected. This is to
258                prevent infinite recursive call caused by symlink.
259
260        @return: A ResultInfo instance containing the directory summary.
261        """
262        is_top_level = top_dir is None
263        top_dir = top_dir or parent_dir
264        all_dirs = all_dirs or set()
265
266        # If the given parent_dir is a file and name is ROOT_DIR, that means
267        # the ResultInfo is for a single file with root directory of the default
268        # ROOT_DIR.
269        if not os.path.isdir(parent_dir) and name == utils_lib.ROOT_DIR:
270            root_dir = os.path.dirname(parent_dir)
271            dir_info = ResultInfo(parent_dir=root_dir,
272                                  name=utils_lib.ROOT_DIR)
273            dir_info.add_file(os.path.basename(parent_dir))
274            return dir_info
275
276        dir_info = ResultInfo(parent_dir=parent_dir,
277                              name=name,
278                              parent_result_info=parent_result_info)
279
280        path = os.path.join(parent_dir, name)
281        if os.path.isdir(path):
282            real_path = os.path.realpath(path)
283            # The assumption here is that results are copied back to drone by
284            # copying the symlink, not the content, which is true with currently
285            # used rsync in cros_host.get_file call.
286            # Skip scanning the child folders if any of following condition is
287            # true:
288            # 1. The directory is a symlink and link to a folder under `top_dir`
289            # 2. The directory was scanned already.
290            if ((os.path.islink(path) and real_path.startswith(top_dir)) or
291                real_path in all_dirs):
292                return dir_info
293            all_dirs.add(real_path)
294            for f in sorted(os.listdir(path)):
295                dir_info.files.append(ResultInfo.build_from_path(
296                        parent_dir=path,
297                        name=f,
298                        parent_result_info=dir_info,
299                        top_dir=top_dir,
300                        all_dirs=all_dirs))
301
302        # Update all directory's original size at the end of the tree building.
303        if is_top_level:
304            dir_info.update_dir_original_size()
305
306        return dir_info
307
308    @property
309    def details(self):
310        """Get the details of the result.
311
312        @return: A dictionary of size and sub-directory information.
313        """
314        return self._details
315
316    @property
317    def is_dir(self):
318        """Get if the result is a directory.
319        """
320        return self._is_dir
321
322    @property
323    def name(self):
324        """Name of the result.
325        """
326        return self._name
327
328    @property
329    def path(self):
330        """Full path to the result.
331        """
332        return self._path
333
334    @property
335    def files(self):
336        """All files or sub-directories of the result.
337
338        @return: A list of ResultInfo objects.
339        @raise ResultInfoError: If the result is not a directory.
340        """
341        if not self.is_dir:
342            raise ResultInfoError('%s is not a directory.' % self.path)
343        return self.details[utils_lib.DIRS]
344
345    @property
346    def size(self):
347        """Physical size in bytes for the result file.
348
349        @raise ResultInfoError: If the result is a directory.
350        """
351        if self.is_dir:
352            raise ResultInfoError(
353                    '`size` property does not support directory. Try to use '
354                    '`original_size` property instead.')
355        return result_info_lib.get_file_size(self._path)
356
357    @property
358    def original_size(self):
359        """The original size in bytes of the result before it's throttled.
360        """
361        return self.details[utils_lib.ORIGINAL_SIZE_BYTES]
362
363    @original_size.setter
364    def original_size(self, value):
365        """Set the original size in bytes of the result.
366
367        @param value: The original size in bytes of the result.
368        """
369        self.details[utils_lib.ORIGINAL_SIZE_BYTES] = value
370        # Update the size of parent result infos if the object is already
371        # initialized.
372        if self._initialized and self._parent_result_info is not None:
373            self._parent_result_info.update_original_size()
374
375    @property
376    def trimmed_size(self):
377        """The size in bytes of the result after it's throttled.
378        """
379        return self.details.get(utils_lib.TRIMMED_SIZE_BYTES,
380                                self.original_size)
381
382    @trimmed_size.setter
383    def trimmed_size(self, value):
384        """Set the trimmed size in bytes of the result.
385
386        @param value: The trimmed size in bytes of the result.
387        """
388        self.details[utils_lib.TRIMMED_SIZE_BYTES] = value
389        # Update the size of parent result infos if the object is already
390        # initialized.
391        if self._initialized and self._parent_result_info is not None:
392            self._parent_result_info.update_trimmed_size()
393
394    @property
395    def collected_size(self):
396        """The collected size in bytes of the result.
397
398        The file is throttled on the dut, so the number of bytes collected from
399        dut is default to the trimmed_size. If a file is modified between
400        multiple result collections and is collected multiple times during the
401        test run, the collected_size will be the sum of the multiple
402        collections. Therefore, its value will be greater than the trimmed_size
403        of the last copy.
404        """
405        return self.details.get(utils_lib.COLLECTED_SIZE_BYTES,
406                                self.trimmed_size)
407
408    @collected_size.setter
409    def collected_size(self, value):
410        """Set the collected size in bytes of the result.
411
412        @param value: The collected size in bytes of the result.
413        """
414        self.details[utils_lib.COLLECTED_SIZE_BYTES] = value
415        # Update the size of parent result infos if the object is already
416        # initialized.
417        if self._initialized and self._parent_result_info is not None:
418            self._parent_result_info.update_collected_size()
419
420    @property
421    def is_collected_size_recorded(self):
422        """Flag to indicate if the result has collected size set.
423
424        This flag is used to avoid unnecessary entry in result details, as the
425        default value of collected size is the trimmed size. Removing the
426        redundant information helps to reduce the size of the json file.
427        """
428        return utils_lib.COLLECTED_SIZE_BYTES in self.details
429
430    @property
431    def parent_result_info(self):
432        """The result info of the parent directory.
433        """
434        return self._parent_result_info
435
436    def add_file(self, name, original_info=None):
437        """Add a file to the result.
438
439        @param name: Name of the file.
440        @param original_info: A dictionary of the file's size and sub-directory
441                information.
442        """
443        self.details[utils_lib.DIRS].append(
444                ResultInfo(parent_dir=self._path,
445                           name=name,
446                           parent_result_info=self,
447                           original_info=original_info))
448        # After a new ResultInfo is added, update the sizes if the object is
449        # already initialized.
450        if self._initialized:
451            self.update_sizes()
452
453    def remove_file(self, name):
454        """Remove a file with the given name from the result.
455
456        @param name: Name of the file to be removed.
457        """
458        self.files.remove(self.get_file(name))
459        # After a new ResultInfo is removed, update the sizes if the object is
460        # already initialized.
461        if self._initialized:
462            self.update_sizes()
463
464    def get_file_names(self):
465        """Get a set of all the files under the result.
466        """
467        return set([list(f.keys())[0] for f in self.files])
468
469    def get_file(self, name):
470        """Get a file with the given name under the result.
471
472        @param name: Name of the file.
473        @return: A ResultInfo object of the file.
474        @raise ResultInfoError: If the result is not a directory, or the file
475                with the given name is not found.
476        """
477        if not self.is_dir:
478            raise ResultInfoError('%s is not a directory. Can\'t locate file '
479                                  '%s' % (self.path, name))
480        for file_info in self.files:
481            if file_info.name == name:
482                return file_info
483        raise ResultInfoError('Can\'t locate file %s in directory %s' %
484                              (name, self.path))
485
486    def convert_to_dir(self):
487        """Convert the result file to a directory.
488
489        This happens when a result file was overwritten by a directory. The
490        conversion will reset the details of this result to be a directory,
491        and save the collected_size to attribute `_previous_collected_size`,
492        so it can be counted when merging multiple result infos.
493
494        @raise ResultInfoError: If the result is already a directory.
495        """
496        if self.is_dir:
497            raise ResultInfoError('%s is already a directory.' % self.path)
498        # The size that's collected before the file was replaced as a directory.
499        collected_size = self.collected_size
500        self._is_dir = True
501        self.details[utils_lib.DIRS] = []
502        self.original_size = 0
503        self.trimmed_size = 0
504        self._previous_collected_size = collected_size
505        self.collected_size = collected_size
506
507    def update_original_size(self, skip_parent_update=False):
508        """Update the original size of the result and trigger its parent to
509        update.
510
511        @param skip_parent_update: True to skip updating parent directory's
512                original size. Default is set to False.
513        """
514        if self.is_dir:
515            self.original_size = sum([
516                    f.original_size for f in self.files])
517        elif self.original_size is None:
518            # Only set original_size if it's not initialized yet.
519            self.orginal_size = self.size
520
521        # Update the size of parent result infos.
522        if not skip_parent_update and self._parent_result_info is not None:
523            self._parent_result_info.update_original_size()
524
525    def update_trimmed_size(self):
526        """Update the trimmed size of the result and trigger its parent to
527        update.
528        """
529        if self.is_dir:
530            new_trimmed_size = sum([f.trimmed_size for f in self.files])
531        else:
532            new_trimmed_size = self.size
533
534        # Only set trimmed_size if the value is changed or different from the
535        # original size.
536        if (new_trimmed_size != self.original_size or
537            new_trimmed_size != self.trimmed_size):
538            self.trimmed_size = new_trimmed_size
539
540        # Update the size of parent result infos.
541        if self._parent_result_info is not None:
542            self._parent_result_info.update_trimmed_size()
543
544    def update_collected_size(self):
545        """Update the collected size of the result and trigger its parent to
546        update.
547        """
548        if self.is_dir:
549            new_collected_size = (
550                    self._previous_collected_size +
551                    sum([f.collected_size for f in self.files]))
552        else:
553            new_collected_size = self.size
554
555        # Only set collected_size if the value is changed or different from the
556        # trimmed size or existing collected size.
557        if (new_collected_size != self.trimmed_size or
558            new_collected_size != self.collected_size):
559            self.collected_size = new_collected_size
560
561        # Update the size of parent result infos.
562        if self._parent_result_info is not None:
563            self._parent_result_info.update_collected_size()
564
565    def update_sizes(self):
566        """Update all sizes information of the result.
567        """
568        self.update_original_size()
569        self.update_trimmed_size()
570        self.update_collected_size()
571
572    def set_parent_result_info(self, parent_result_info, update_sizes=True):
573        """Set the parent result info.
574
575        It's used when a ResultInfo object is moved to a different file
576        structure.
577
578        @param parent_result_info: A ResultInfo object for the parent directory.
579        @param update_sizes: True to update the parent's size information. Set
580                it to False to delay the update for better performance.
581        """
582        self._parent_result_info = parent_result_info
583        # As the parent reference changed, update all sizes of the parent.
584        if parent_result_info and update_sizes:
585            self._parent_result_info.update_sizes()
586
587    def merge(self, new_info, is_final=False):
588        """Merge a ResultInfo instance to the current one.
589
590        Update the old directory's ResultInfo with the new one. Also calculate
591        the total size of results collected from the client side based on the
592        difference between the two ResultInfo.
593
594        When merging with newer collected results, any results not existing in
595        the new ResultInfo or files with size different from the newer files
596        collected are considered as extra results collected or overwritten by
597        the new results.
598        Therefore, the size of the collected result should include such files,
599        and the collected size can be larger than trimmed size.
600        As an example:
601        current: {'file1': {TRIMMED_SIZE_BYTES: 1024,
602                            ORIGINAL_SIZE_BYTES: 1024,
603                            COLLECTED_SIZE_BYTES: 1024}}
604        This means a result `file1` of original size 1KB was collected with size
605        of 1KB byte.
606        new_info: {'file1': {TRIMMED_SIZE_BYTES: 1024,
607                             ORIGINAL_SIZE_BYTES: 2048,
608                             COLLECTED_SIZE_BYTES: 1024}}
609        This means a result `file1` of 2KB was trimmed down to 1KB and was
610        collected with size of 1KB byte.
611        Note that the second result collection has an updated result `file1`
612        (because of the different ORIGINAL_SIZE_BYTES), and it needs to be
613        rsync-ed to the drone. Therefore, the merged ResultInfo will be:
614        {'file1': {TRIMMED_SIZE_BYTES: 1024,
615                   ORIGINAL_SIZE_BYTES: 2048,
616                   COLLECTED_SIZE_BYTES: 2048}}
617        Note that:
618        * TRIMMED_SIZE_BYTES is still at 1KB, which reflects the actual size of
619          the file be collected.
620        * ORIGINAL_SIZE_BYTES is updated to 2KB, which is the size of the file
621          in the new result `file1`.
622        * COLLECTED_SIZE_BYTES is 2KB because rsync will copy `file1` twice as
623          it's changed.
624
625        The only exception is that the new ResultInfo's ORIGINAL_SIZE_BYTES is
626        the same as the current ResultInfo's TRIMMED_SIZE_BYTES. That means the
627        file was trimmed in the current ResultInfo and the new ResultInfo is
628        collecting the trimmed file. Therefore, the merged summary will keep the
629        data in the current ResultInfo.
630
631        @param new_info: New ResultInfo to be merged into the current one.
632        @param is_final: True if new_info is built from the final result folder.
633                Default is set to False.
634        """
635        new_files = new_info.get_file_names()
636        old_files = self.get_file_names()
637        # A flag to indicate if the sizes need to be updated. It's required when
638        # child result_info is added to `self`.
639        update_sizes_pending = False
640        for name in new_files:
641            new_file = new_info.get_file(name)
642            if not name in old_files:
643                # A file/dir exists in new client dir, but not in the old one,
644                # which means that the file or a directory is newly collected.
645                self.files.append(new_file)
646                # Once parent_result_info is changed, new_file object will no
647                # longer associated with `new_info` object.
648                new_file.set_parent_result_info(self, update_sizes=False)
649                update_sizes_pending = True
650            elif new_file.is_dir:
651                # `name` is a directory in the new ResultInfo, try to merge it
652                # with the current ResultInfo.
653                old_file = self.get_file(name)
654
655                if not old_file.is_dir:
656                    # If `name` is a file in the current ResultInfo but a
657                    # directory in new ResultInfo, the file in the current
658                    # ResultInfo will be overwritten by the new directory by
659                    # rsync. Therefore, force it to be an empty directory in
660                    # the current ResultInfo, so that the new directory can be
661                    # merged.
662                    old_file.convert_to_dir()
663
664                old_file.merge(new_file, is_final)
665            else:
666                old_file = self.get_file(name)
667
668                # If `name` is a directory in the current ResultInfo, but a file
669                # in the new ResultInfo, rsync will fail to copy the file as it
670                # can't overwrite an directory. Therefore, skip the merge.
671                if old_file.is_dir:
672                    continue
673
674                new_size = new_file.original_size
675                old_size = old_file.original_size
676                new_trimmed_size = new_file.trimmed_size
677                old_trimmed_size = old_file.trimmed_size
678
679                # Keep current information if the sizes are not changed.
680                if (new_size == old_size and
681                    new_trimmed_size == old_trimmed_size):
682                    continue
683
684                # Keep current information if the newer size is the same as the
685                # current trimmed size, and the file is not trimmed in new
686                # ResultInfo. That means the file was trimmed earlier and stays
687                # the same when collecting the information again.
688                if (new_size == old_trimmed_size and
689                    new_size == new_trimmed_size):
690                    continue
691
692                # If the file is merged from the final result folder to an older
693                # ResultInfo, it's not considered to be trimmed if the size is
694                # not changed. The reason is that the file on the server side
695                # does not have the info of its original size.
696                if is_final and new_trimmed_size == old_trimmed_size:
697                    continue
698
699                # `name` is a file, and both the original_size and trimmed_size
700                # are changed, that means the file is overwritten, so increment
701                # the collected_size.
702                # Before trimming is implemented, collected_size is the
703                # value of original_size.
704                new_collected_size = new_file.collected_size
705                old_collected_size = old_file.collected_size
706
707                old_file.collected_size = (
708                        new_collected_size + old_collected_size)
709                # Only set trimmed_size if one of the following two conditions
710                # are true:
711                # 1. In the new summary the file's trimmed size is different
712                #    from the original size, which means the file was trimmed
713                #    in the new summary.
714                # 2. The original size in the new summary equals the trimmed
715                #    size in the old summary, which means the file was trimmed
716                #    again in the new summary.
717                if (new_size == old_trimmed_size or
718                    new_size != new_trimmed_size):
719                    old_file.trimmed_size = new_file.trimmed_size
720                old_file.original_size = new_size
721
722        if update_sizes_pending:
723            self.update_sizes()
724
725
726# An empty directory, used to compare with a ResultInfo.
727EMPTY = ResultInfo(parent_dir='',
728                   original_info={'': {utils_lib.ORIGINAL_SIZE_BYTES: 0,
729                                       utils_lib.DIRS: []}})
730
731
732def save_summary(summary, json_file):
733    """Save the given directory summary to a file.
734
735    @param summary: A ResultInfo object for a result directory.
736    @param json_file: Path to a json file to save to.
737    """
738    with open(json_file, 'w') as f:
739        json.dump(summary, f)
740
741
742def load_summary_json_file(json_file):
743    """Load result info from the given json_file.
744
745    @param json_file: Path to a json file containing a directory summary.
746    @return: A ResultInfo object containing the directory summary.
747    """
748    with open(json_file, 'r') as f:
749        summary = json.load(f)
750
751    # Convert summary to ResultInfo objects
752    result_dir = os.path.dirname(json_file)
753    return ResultInfo(parent_dir=result_dir, original_info=summary)
754