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