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