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