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