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