1#/usr/bin/env python3 2# 3# Copyright (C) 2018 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may not 6# use this file except in compliance with the License. You may obtain a copy of 7# the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations under 15# the License. 16 17# 18# Copyright 2009 Google Inc. All Rights Reserved. 19"""Lightweight Sponge client, supporting upload via the HTTP Redirector. 20 21Does not depend on protobufs, Stubby, works on Windows, builds without blaze. 22""" 23 24__author__ = 'klm@google.com (Michael Klepikov)' 25 26import collections 27import os 28import re 29import socket 30import time 31 32try: 33 import httpclient as httplib 34except ImportError: 35 import httplib 36 37try: 38 import StringIO 39except ImportError: 40 from io import StringIO 41 42try: 43 import google3 # pylint: disable=g-import-not-at-top 44 from google3.testing.coverage.util import bitfield # pylint: disable=g-import-not-at-top 45except ImportError: 46 pass # Running outside of google3 47 48import SimpleXMLWriter # pylint: disable=g-import-not-at-top 49 50 51class Entity(object): 52 """Base class for all Sponge client entities. Provides XML s11n basics.""" 53 54 def WriteXmlToStream(self, ostream, encoding='UTF-8'): 55 """Writes out all attributes with string/numeric value to supplied ostream. 56 57 Args: 58 ostream: A file or file-like object. This object must implement a write 59 method. 60 encoding: Optionally specify encoding to be used. 61 """ 62 xml_writer = SimpleXMLWriter.XMLWriter(ostream, encoding) 63 self.WriteXml(xml_writer) 64 65 def WriteXml(self, xml_writer): 66 """Writes out all attributes that have a string or numeric value. 67 68 Args: 69 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 70 """ 71 for attr_name in dir(self): # Guaranteed sorted alphabetically 72 assert attr_name 73 if attr_name.startswith( 74 '_') or attr_name[0].upper() == attr_name[0]: 75 continue # Skip non-public attributes and public constants 76 if hasattr(self, '_permitted_attributes'): 77 assert attr_name in self._permitted_attributes 78 if (hasattr(self, '_custom_write_attributes') 79 and attr_name in self._custom_write_attributes): 80 # An attribute that has custom serialization code 81 continue 82 value = self.__getattribute__(attr_name) 83 if callable(value): 84 continue # Skip methods 85 Entity._WriteValue(xml_writer, attr_name, value) 86 87 def GetXmlString(self): 88 """Returns a string with XML produced by WriteXml().""" 89 xml_out = StringIO.StringIO() 90 self.WriteXmlToStream(xml_out) 91 xml_str = xml_out.getvalue() 92 xml_out.close() 93 return xml_str 94 95 @staticmethod 96 def _WriteValue(xml_writer, name, value): 97 if value is None: 98 return # Do not serialize None (but do serialize 0 or empty string) 99 elif isinstance(value, unicode): 100 xml_writer.element(name, value) # Will write out as UTF-8 101 elif isinstance(value, str): 102 # A non-Unicode string. By default the encoding is 'ascii', 103 # where 8-bit characters cause an encoding exception 104 # when a protobuf encodes itself on the HTTP Redirector side. 105 # Force 'latin' encoding, which allows 8-bit chars. 106 # Still it's only a guess which could be wrong, so use errors='replace' 107 # to produce an 'invalid character' Unicode placeholder in such cases. 108 # For the caller, the cleanest thing to do is pass a proper 109 # Unicode string if it may contain international characters. 110 xml_writer.element( 111 name, unicode(value, encoding='latin', errors='replace')) 112 elif isinstance(value, bool): 113 # Careful! Check for this before isinstance(int) -- true for bools 114 xml_writer.element(name, str(value).lower()) 115 elif (isinstance(value, int) or isinstance(value, long) 116 or isinstance(value, float)): 117 xml_writer.element(name, str(value)) 118 elif hasattr(value, 'WriteXml'): 119 # An object that knows how to write itself 120 xml_writer.start(name) 121 value.WriteXml(xml_writer) 122 xml_writer.end() 123 elif isinstance(value, list) or isinstance(value, tuple): 124 # Sequence names are often plural, but the element name must be single 125 if name.endswith('s'): 126 value_element_name = name[0:len(name) - 1] 127 else: 128 value_element_name = name 129 for sequence_value in value: 130 Entity._WriteValue(xml_writer, value_element_name, 131 sequence_value) 132 elif hasattr(value, 'iteritems'): # A mapping type 133 # Map names are often plural, but the element name must be single 134 if name.endswith('s'): 135 map_element_name = name[0:len(name) - 1] 136 else: 137 map_element_name = name 138 Entity._WriteNameValuesXml(xml_writer, map_element_name, value, 139 'name', 'value') 140 141 @staticmethod 142 def _WriteNameValuesXml(xml_writer, element_name, name_value_dict, 143 name_elem, value_elem): 144 """Writes a dict as XML elements with children as keys (names) and values. 145 146 Args: 147 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 148 element_name: name of enclosing element for the name-value pair elements. 149 name_value_dict: the dict to write. 150 name_elem: name of the "name" element. 151 value_elem: name of the "value" element. 152 """ 153 if name_value_dict: 154 for name in sorted( 155 name_value_dict): # Guarantee order for testability 156 value = name_value_dict[name] 157 xml_writer.start(element_name) 158 Entity._WriteValue(xml_writer, name_elem, name) 159 Entity._WriteValue(xml_writer, value_elem, value) 160 xml_writer.end() 161 162 163class LcovUtils(object): 164 """Just groups Lcov handling.""" 165 166 @staticmethod 167 def GetFilename(lcov_section): 168 return lcov_section.split('\n', 1)[0].strip()[3:] 169 170 @staticmethod 171 def LcovSectionToBitFields(lcov_section): 172 """Fill in bit fields that represent covered and instrumented lines. 173 174 Note that lcov line numbers start from 1 while sponge expects line numbers 175 to start from 0, hence the line_num-1 is required. 176 177 Args: 178 lcov_section: string, relevant section of lcov 179 180 Returns: 181 Tuple of google3.testing.coverage.util.bitfield objects. First bitfield 182 represents lines covered. Second bitfield represents total lines 183 instrumented. 184 """ 185 covered_bf = bitfield.BitField() 186 instrumented_bf = bitfield.BitField() 187 for line in lcov_section.split('\n'): 188 if line.startswith('DA:'): 189 line_num, times_hit = line.strip()[3:].split(',') 190 instrumented_bf.SetBit(int(line_num) - 1) 191 if times_hit != '0': 192 covered_bf.SetBit(int(line_num) - 1) 193 elif line.startswith('FN:'): 194 pass # Function coverage will be supported soon. 195 return covered_bf, instrumented_bf 196 197 @staticmethod 198 def UrlEncode(bit_field): 199 """Convert bit field into url-encoded string of hex representation.""" 200 if not bit_field.CountBitsSet(): 201 return '%00' 202 else: 203 ret_str = '' 204 for c in bit_field.Get(): 205 ret_str += '%%%02x' % ord(c) 206 return ret_str.upper() 207 208 @staticmethod 209 def WriteBitfieldXml(xml_writer, name, value): 210 encoded_value = LcovUtils.UrlEncode(value) 211 xml_writer.element( 212 name, unicode(encoded_value, encoding='latin', errors='replace')) 213 214 215class FileCoverage(Entity): 216 """Represents Sponge FileCoverage. 217 218 instrumented_lines and executed_lines are bit fields with following format: 219 Divide line number by 8 to get index into string. 220 Mod line number by 8 to get bit number (0 = LSB, 7 = MSB). 221 222 Attributes: 223 file_name: name of the file this entry represents. 224 location: the location of the file: PERFORCE, MONDRIAN, UNKNOWN. 225 revision: stores the revision number of the file when location is PERFORCE. 226 instrumented_lines: bitfield of line numbers that have been instrumented 227 executed_lines: bitfield of line numbers that have been executed 228 md5: string. Hex representation of the md5 checksum for the file 229 "file_name". This should only be set if file_name is open in the 230 client. 231 pending_cl: string. CL containing the file "file_name" if it is checked out 232 at the time this invocation is sent out. Should only be set if 233 location is MONDRIAN. 234 sourcerer_depot: string. [optional] The sourcerer depot to use in coverage 235 tab. Only required if your code is stored in one of the PerforceN 236 servers and therefore has it's own Sourcerer instance. For example, 237 Perforce11 code should set sourcerer_depot to "s11". 238 """ 239 240 # location 241 PERFORCE = 0 242 MONDRIAN = 1 243 UNKNOWN = 2 244 245 def __init__(self): 246 super(FileCoverage, self).__init__() 247 self.file_name = None 248 self.location = None 249 self.revision = None 250 self.md5 = None 251 self.pending_cl = None 252 self.executed_lines = None 253 self.instrumented_lines = None 254 self.sourcerer_depot = None 255 self._custom_write_attributes = [ 256 'executed_lines', 'instrumented_lines' 257 ] 258 259 def WriteXml(self, xml_writer): 260 """Writes this object as XML suitable for Sponge HTTP Redirector. 261 262 Args: 263 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 264 """ 265 super(FileCoverage, self).WriteXml(xml_writer) 266 for attr_name in self._custom_write_attributes: 267 value = self.__getattribute__(attr_name) 268 if value: 269 LcovUtils.WriteBitfieldXml(xml_writer, attr_name, value) 270 271 def Combine(self, other_file_coverage): 272 """Combines 2 FileCoverage objects. 273 274 This method expects all fields of the 2 FileCoverage objects to be identical 275 except for the executed_lines and instrumented_lines fields which it will 276 combine into 1 by performing logical OR operation on executed_lines and 277 instrumented_lines bitfields. All other fields are copied directly from 278 source. 279 280 Args: 281 other_file_coverage: FileCoverage object to combine with 282 283 Returns: 284 The combined FileCoverage object 285 """ 286 assert self.file_name == other_file_coverage.file_name 287 assert self.location == other_file_coverage.location 288 assert self.revision == other_file_coverage.revision 289 assert self.md5 == other_file_coverage.md5 290 assert self.pending_cl == other_file_coverage.pending_cl 291 292 result_file_coverage = FileCoverage() 293 result_file_coverage.file_name = self.file_name 294 result_file_coverage.location = self.location 295 result_file_coverage.revision = self.revision 296 result_file_coverage.md5 = self.md5 297 result_file_coverage.pending_cl = self.pending_cl 298 299 result_file_coverage.executed_lines = self.executed_lines.Or( 300 other_file_coverage.executed_lines) 301 result_file_coverage.instrumented_lines = self.instrumented_lines.Or( 302 other_file_coverage.instrumented_lines) 303 304 return result_file_coverage 305 306 def FromLcovSection(self, lcov_section): 307 """Fill in coverage from relevant lcov section. 308 309 An lcov section starts with a line starting with 'SF:' followed by filename 310 of covered file and is followed by 1 or more lines of coverage data starting 311 with 'DA:' or 'FN:'. 312 313 'DA:'lines have the format: 314 'DA: line_num, times_covered' 315 316 line_num is the line number of source file starting from 1. 317 times_covered is the number of times the line was covered, starting from 0. 318 319 'FN:' is for function coverage and is not supported yet. 320 321 An example section would look like this: 322 SF:/Volumes/BuildData/PulseData/data/googleclient/picasa4/yt/safe_str.h 323 DA:1412,12 324 DA:1413,12 325 DA:1414,0 326 DA:1415,0 327 328 Args: 329 lcov_section: string, relevant section of lcov file. 330 """ 331 if lcov_section: 332 assert lcov_section.startswith('SF:') 333 334 self.file_name = LcovUtils.GetFilename(lcov_section) 335 self.executed_lines, self.instrumented_lines = ( 336 LcovUtils.LcovSectionToBitFields(lcov_section)) 337 338 339class TargetCodeCoverage(Entity): 340 """Represents Sponge TargetCodeCoverage. 341 342 Attributes: 343 file_coverage: list of FileCoverage object. 344 instrumentation: method of instrumentation: ONTHEFLY, OFFLINE, UNKNOWN 345 """ 346 347 # instrumentation 348 ONTHEFLY = 0 349 OFFLINE = 1 350 UNKNOWN = 2 351 352 def __init__(self): 353 super(TargetCodeCoverage, self).__init__() 354 self.file_coverage = [] 355 self.instrumentation = None 356 357 # Warning: *DO NOT* switch to Python 2.7 OrderedDict. This code needs to 358 # run on Windows and other environments where Python 2.7 may not be 359 # available. 360 self._file_coverage_map = collections.OrderedDict() 361 362 def FromLcovString(self, lcov_str): 363 """Fill in coverage from lcov-formatted string. 364 365 Args: 366 lcov_str: contents of lcov file as string 367 """ 368 for entry in lcov_str.split('end_of_record\n'): 369 file_coverage = FileCoverage() 370 file_coverage.FromLcovSection(entry.strip()) 371 372 if not file_coverage.file_name: 373 continue 374 375 prev_file_coverage = self._file_coverage_map.get( 376 file_coverage.file_name) 377 if prev_file_coverage: 378 self._file_coverage_map[file_coverage.file_name] = ( 379 prev_file_coverage.Combine(file_coverage)) 380 else: 381 self._file_coverage_map[ 382 file_coverage.file_name] = file_coverage 383 384 self.file_coverage = self._file_coverage_map.values() 385 386 def IndexOf(self, filename): 387 """Index of filename in the FileCoverage map. Must exist!""" 388 return self._file_coverage_map.keys().index(filename) 389 390 391class Sample(Entity): 392 """Represents a single data sample within a Metric object. 393 394 Attributes: 395 value: the data value of this sample -- the thing that we measured. 396 timestamp_in_millis: the time when this particular sample was taken. 397 Milliseconds since the Epoch. Not required, but highly recommended for 398 a proper single-CL view in LoadViz that shows all samples of one run. 399 outcome: SUCCESSFUL_OUTCOME or FAILED_OUTCOME. 400 metadata: a dict of arbitrary user defined name-value pairs. 401 For example, when measuring page load times, one can store the page URL 402 under the key "url" in the metadata. 403 """ 404 405 SUCCESSFUL_OUTCOME = 0 406 FAILED_OUTCOME = 1 407 408 def __init__(self): 409 super(Sample, self).__init__() 410 self.value = None 411 self.timestamp_in_millis = None 412 self.outcome = None 413 self.metadata = {} 414 415 416class Percentile(Entity): 417 """Represents a percentile within an Aggregation object. 418 419 Percentile objects only give enough info to filter samples by percentiles, 420 Sponge doesn't store per-percentile means etc. 421 422 Attributes: 423 percentage: upper bracket of the percentile: integer number of percent. 424 Lower bracket is always zero. 425 value: maximum value for the this percentile. 426 """ 427 428 def __init__(self): 429 super(Percentile, self).__init__() 430 self.percentage = None 431 self.value = None 432 433 434class Aggregation(Entity): 435 """Represents aggregated values from samples in a Metric object. 436 437 As also noted in Metric, Sponge would compute a default Aggregation 438 if it's not supplied explicitly with a Metric. Sponge currently computes 439 the following percentiles: 50, 80, 90, 95, 99, with no way to control it. 440 If you want other percentiles, you need to provide the Aggregatioin yourself. 441 442 Attributes: 443 count: the number of samples represented by this aggregation. 444 min: minimum sample value. 445 max: maximum sample value. 446 mean: mean of all sample values. 447 standard_deviation: standard deviation of all sample values. 448 percentiles: a sequence of Percentile objects. 449 error_count: the number of samples with error outcomes. 450 """ 451 452 def __init__(self): 453 super(Aggregation, self).__init__() 454 self.count = None 455 self.min = None 456 self.max = None 457 self.mean = None 458 self.standard_deviation = None 459 self.error_count = None 460 self.percentiles = [] 461 462 463class Metric(Entity): 464 """Represents a single metric under PerformanceData. 465 466 See the comment in PerformanceData about the mapping to sponge.proto. 467 468 Attributes: 469 name: the metric name. 470 time_series: if True, this is a time series, otherwise not a time series. 471 unit: string name of the unit of measure for sample values in this metric. 472 machine_name: hostname where the test was run. 473 If None, use Invocation.hostname. 474 aggregation: an Aggregation object. 475 If None, Sponge will compute it from samples. 476 samples: a sequence of Sample objects. 477 """ 478 479 def __init__(self): 480 super(Metric, self).__init__() 481 self.name = None 482 self.time_series = True 483 self.unit = None 484 self.machine_name = None 485 self.aggregation = None 486 self.samples = [] 487 488 489class PerformanceData(Entity): 490 """Represents Sponge PerformanceData, only moved under a TargetResult. 491 492 Currently sponge.proto defines PerformanceData as a top level object, 493 stored in a separate table from Invocations. There is an idea to move it 494 under a TargetResult, allowing it to have labels and generally play 495 by the same rules as all other test runs -- coverage etc. 496 497 So far the interim solution is to try to have PerformanceData under 498 a TargetResult only in sponge_client_lite, and do an on the fly 499 conversion to sponge.proto structures in the HTTP Redirector. 500 If all goes well there, then a similar conversion in the other direction 501 (top level PerformanceData -> PerformanceData under a TargetResult) 502 can be implemented in Sponge Java upload code, together with a data model 503 change, allowing backward compatibility with older performance test clients. 504 505 The mapping of the PerformanceData fields missing here is as follows: 506 id -> Invocation.id 507 timestamp_in_millis -> TargetResult.run_date 508 cl -> Invocation.cl 509 config -> TargetResult.configuration_values 510 user -> Invocation.user 511 description, project_name, project_id -- not mapped, if necessary should 512 be added to Invocation and/or TargetResult, as they are not 513 performance-specific. TODO(klm): discuss use cases with havardb@. 514 515 For LoadViz to work properly, Invocation.cl must be supplied even though 516 it's formally optional in the Invocation. It doesn't have to be an actual 517 Perforce CL number, could be an arbitrary string, but these strings must 518 sort in the chronological order -- e.g. may represent a date and time, 519 for example may use an ISO date+time string notation of the run_date. 520 521 Attributes: 522 benchmark: benchmark name -- the most important ID in LoadViz. 523 Must not be None for results to be usable in LoadViz. 524 experiment: experiment name. 525 thread_count: for load tests, the number of concurrent threads. 526 aggregator_strategy: NONE or V1 or V1_NO_DOWNSAMPLE. 527 metrics: a sequence of Metric objects. 528 """ 529 530 NONE = 0 531 V1 = 1 532 V1_NO_DOWNSAMPLE = 2 533 534 def __init__(self): 535 super(PerformanceData, self).__init__() 536 self.benchmark = None 537 self.experiment = None 538 self.thread_count = None 539 self.aggregator_strategy = None 540 self.metrics = [] 541 542 543class TestFault(Entity): 544 """Test failure/error data. 545 546 Attributes: 547 message: message for the failure/error. 548 exception_type: the type of failure/error. 549 detail: details of the failure/error. 550 """ 551 552 def __init__(self): 553 super(TestFault, self).__init__() 554 555 self._permitted_attributes = set( 556 ['message', 'exception_type', 'detail']) 557 self.message = None 558 self.exception_type = None 559 self.detail = None 560 561 562class TestResult(Entity): 563 """Test case data. 564 565 Attributes: 566 child: List of TestResult representing test suites or test cases 567 name: Test result name 568 class_name: Required for test cases, otherwise not 569 was_run: true/false, default true, optional 570 run_duration_millis: - 571 property: List of TestProperty entities. 572 test_case_count: number of test cases 573 failure_count: number of failures 574 error_count: number of errors 575 disabled_count: number of disabled tests 576 test_file_coverage: List of TestCaseFileCoverage 577 test_failure: List of TestFault objects describing test failures 578 test_error: List of TestFault objects describing test errors 579 result: The result of running a test case: COMPLETED, INTERRUPTED, etc 580 """ 581 582 # result 583 COMPLETED = 0 584 INTERRUPTED = 1 585 CANCELLED = 2 586 FILTERED = 3 587 SKIPPED = 4 588 SUPPRESSED = 5 589 590 # Match DA lines claiming nonzero execution count. 591 _lcov_executed_re = re.compile(r'^DA:\d+,[1-9][0-9]*', re.MULTILINE) 592 593 def __init__(self): 594 super(TestResult, self).__init__() 595 596 self._permitted_attributes = set([ 597 'child', 'name', 'class_name', 'was_run', 'run_duration_millis', 598 'property', 'test_case_count', 'failure_count', 'error_count', 599 'disabled_count', 'test_file_coverage', 'test_failure', 600 'test_error', 'result' 601 ]) 602 self.child = [] 603 self.name = None 604 self.class_name = None 605 self.was_run = True 606 self.run_duration_millis = None 607 self.property = [] 608 self.test_case_count = None 609 self.failure_count = None 610 self.error_count = None 611 self.disabled_count = None 612 self.test_file_coverage = [] 613 self.test_error = [] 614 self.test_failure = [] 615 self.result = None 616 617 def FromLcovString(self, lcov_str, target_code_coverage): 618 """Fill in hit coverage from lcov-formatted string and target_code_coverage. 619 620 Ignores files with zero hit bitmaps; presumes target_code_coverage is final 621 for the purposes of determining the index of filenames. 622 623 Args: 624 lcov_str: contents of lcov file as string 625 target_code_coverage: TargetCodeCoverage for filename indexing 626 """ 627 for entry in lcov_str.split('end_of_record\n'): 628 629 if not TestResult._lcov_executed_re.search(entry): 630 continue 631 632 test_file_coverage = TestCaseFileCoverage() 633 test_file_coverage.FromLcovSection(entry.strip(), 634 target_code_coverage) 635 636 self.test_file_coverage.append(test_file_coverage) 637 638 639class TestProperty(Entity): 640 """Test property data. 641 642 Attributes: 643 key: A string representing the property key. 644 value: A string representing the property value. 645 """ 646 647 def __init__(self): 648 super(TestProperty, self).__init__() 649 self._permitted_attributes = set(['key', 'value']) 650 self.key = None 651 self.value = None 652 653 654class TestCaseFileCoverage(Entity): 655 """Test case file coverage data. 656 657 Attributes: 658 file_coverage_index: index into associated test target's file coverage. 659 executed_lines: bitfield representing executed lines, as for FileCoverage. 660 zipped_executed_lines: zip of executed_lines data, if smaller. 661 """ 662 663 def __init__(self): 664 super(TestCaseFileCoverage, self).__init__() 665 666 self._permitted_attributes = set( 667 ['file_coverage_index', 'executed_lines', 'zipped_executed_lines']) 668 669 self.file_coverage_index = None 670 self.executed_lines = 0 671 self.zipped_executed_lines = 0 672 self._custom_write_attributes = [ 673 'executed_lines', 'zipped_executed_lines' 674 ] 675 676 def WriteXml(self, xml_writer): 677 """Writes this object as XML suitable for Sponge HTTP Redirector. 678 679 Args: 680 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 681 """ 682 super(TestCaseFileCoverage, self).WriteXml(xml_writer) 683 for attr_name in self._custom_write_attributes: 684 value = self.__getattribute__(attr_name) 685 if value: 686 LcovUtils.WriteBitfieldXml(xml_writer, attr_name, value) 687 # TODO(weasel): Mmmaybe lift bitfield handling to the base class. 688 689 def FromLcovSection(self, lcov_section, tcc): 690 if lcov_section: 691 assert lcov_section.startswith('SF:') 692 693 file_name = LcovUtils.GetFilename(lcov_section) 694 self.file_coverage_index = tcc.IndexOf(file_name) 695 self.executed_lines, unused_instrumented_lines = ( 696 LcovUtils.LcovSectionToBitFields(lcov_section)) 697 # TODO(weasel): compress executed_lines to zipped_* if smaller. 698 699 700class GoogleFilePointer(Entity): 701 """Represents a Google File system path. 702 703 Attributes: 704 name: str name for use by Sponge 705 path: str containing the target Google File. 706 length: integer size of the file; used purely for display purposes. 707 """ 708 709 def __init__(self, name, path, length): 710 super(GoogleFilePointer, self).__init__() 711 self.name = name 712 self.path = path 713 self.length = length 714 715 def WriteXml(self, xml_writer): 716 """Writes this object as XML suitable for Sponge HTTP Redirector. 717 718 Args: 719 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 720 """ 721 Entity._WriteValue(xml_writer, 'name', self.name) 722 xml_writer.start('google_file_pointer') 723 Entity._WriteValue(xml_writer, 'path', self.path) 724 Entity._WriteValue(xml_writer, 'length', self.length) 725 xml_writer.end() 726 727 728class TargetResult(Entity): 729 """Represents Sponge TargetResult. 730 731 Attributes: 732 index: index of the target result within its parent Invocation. 733 Needed only for update requests, not for initial creation. 734 run_date: execution start timestamp in milliseconds. 735 build_target: the name of the build target that was executed. 736 size: one of size constants: SMALL, MEDIUM, LARGE, OTHER_SIZE, ENORMOUS. 737 environment: how we ran: FORGE, LOCAL_*, OTHER_*, UNKNOWN_*. 738 status: test outcome: PASSED, FAILED, etc. 739 test_result: tree of TestResults representing test suites and test cases. 740 language: programming language of the source code: CC, JAVA, etc. 741 run_duration_millis: execution duration in milliseconds. 742 status_details: a string explaining the status in more detail. 743 attempt_number: for flaky reruns, the number of the run attempt. Start at 1. 744 total_attempts: for flaky reruns, the total number of run attempts. 745 coverage: a TargetCodeCoverage object. 746 performance_data: a PerformanceData object. 747 configuration_values: a dict of test configuration parameters. 748 type: the type of target: TEST, BINARY, LIBRARY, APPLICATION. 749 large_texts: a dict of logs associated with this run. A magic key 'XML Log' 750 allows to upload GUnit/JUnit XML and auto-convert it to TestResults. 751 large_text_pointers: a list of GoogleFilePointers - distinction for 752 formatting only, these are conceptually the same as large_texts. 753 """ 754 755 # size - if you update these values ensure to also update the appropriate 756 # enum list in uploader_recommended_options.py 757 SMALL = 0 758 MEDIUM = 1 759 LARGE = 2 760 OTHER_SIZE = 3 761 ENORMOUS = 4 762 763 # environment 764 FORGE = 0 765 LOCAL_PARALLEL = 1 766 LOCAL_SEQUENTIAL = 2 767 OTHER_ENVIRONMENT = 3 768 UNKNOWN_ENVIRONMENT = 4 769 770 # status - if you update these values ensure to also update the appropriate 771 # enum list in uploader_optional_options.py 772 PASSED = 0 773 FAILED = 1 774 CANCELLED_BY_USER = 2 775 ABORTED_BY_TOOL = 3 776 FAILED_TO_BUILD = 4 777 BUILT = 5 778 PENDING = 6 779 UNKNOWN_STATUS = 7 780 INTERNAL_ERROR = 8 781 782 # language - if you update these values ensure to also update the appropriate 783 # enum list in uploader_recommended_options.py 784 UNSPECIFIED_LANGUAGE = 0 785 BORGCFG = 1 786 CC = 2 787 GWT = 3 788 HASKELL = 4 789 JAVA = 5 790 JS = 6 791 PY = 7 792 SH = 8 793 SZL = 9 794 795 # type 796 UNSPECIFIED_TYPE = 0 797 TEST = 1 798 BINARY = 2 799 LIBRARY = 3 800 APPLICATION = 4 801 802 def __init__(self): 803 super(TargetResult, self).__init__() 804 self.index = None 805 self.run_date = long(round(time.time() * 1000)) 806 self.build_target = None 807 self.size = None 808 self.environment = None 809 self.status = None 810 self.test_result = None 811 self.language = None 812 self.run_duration_millis = None 813 self.status_details = None 814 self.attempt_number = None 815 self.total_attempts = None 816 self.coverage = None 817 self.performance_data = None 818 self.configuration_values = {} 819 self.type = None 820 self.large_texts = {} 821 self.large_text_pointers = [] 822 self._custom_write_attributes = ['large_text_pointers'] 823 824 def MarkRunDuration(self): 825 """Assigns run_duration_millis to the current time minus run_date.""" 826 assert self.run_date 827 self.run_duration_millis = long(round( 828 time.time() * 1000)) - self.run_date 829 assert self.run_duration_millis > 0 830 831 def WriteXml(self, xml_writer): 832 """Writes this object as XML suitable for Sponge HTTP Redirector. 833 834 Args: 835 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 836 """ 837 super(TargetResult, self).WriteXml(xml_writer) 838 # Write out GoogleFilePointers as large_text fields 839 for google_file_pointer in self.large_text_pointers: 840 Entity._WriteValue(xml_writer, 'large_text', google_file_pointer) 841 842 843class Invocation(Entity): 844 """Represents a Sponge Invocation. 845 846 Attributes: 847 id: the ID of an invocation to update. 848 Needed only for update requests, not for initial creation. 849 run_date: execution start timestamp in milliseconds 850 user: username. 851 client: P4 client name. 852 cl: P4 changelist ID. 853 hostname: the host where the tests ran. 854 working_dir: the dir where the tests ran. 855 args: command line arguments of the test command. 856 environment_variables: a dict of notable OS environment variables. 857 configuration_values: a dict of test configuration parameters. 858 large_texts: a dict of logs associated with the entire set of target runs. 859 labels: a list of labels associated with this invocation. 860 target_results: a list of TargetResult objects. 861 large_text_pointers: a list of GoogleFilePointers - distinction for 862 formatting only, these are conceptually the same as large_texts. 863 """ 864 865 def __init__(self): 866 super(Invocation, self).__init__() 867 self.id = None 868 self.run_date = long(round(time.time() * 1000)) 869 self.user = None 870 self.target_results = [] 871 self.client = None 872 self.cl = None 873 self.hostname = socket.gethostname().lower() 874 self.working_dir = os.path.abspath(os.curdir) 875 self.args = None 876 self.environment_variables = {} 877 self.configuration_values = {} 878 self.large_texts = {} 879 self.large_text_pointers = [] 880 self.labels = [] 881 self._custom_write_attributes = [ 882 'environment_variables', 883 'large_text_pointers', 884 ] 885 886 def WriteXml(self, xml_writer): 887 """Writes this object as XML suitable for Sponge HTTP Redirector. 888 889 Args: 890 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 891 """ 892 super(Invocation, self).WriteXml(xml_writer) 893 Entity._WriteNameValuesXml( 894 xml_writer, 895 'environment_variable', 896 self.environment_variables, 897 name_elem='variable', 898 value_elem='value') 899 # Write out GoogleFilePointers as large_text fields 900 for google_file_pointer in self.large_text_pointers: 901 Entity._WriteValue(xml_writer, 'large_text', google_file_pointer) 902 903 904# Constants for Uploader.server 905SERVER_PROD = 'backend' 906SERVER_QA = 'backend-qa' 907 908 909class Uploader(Entity): 910 """Uploads Sponge Invocations to the Sponge HTTP Redirector service.""" 911 912 def __init__(self, 913 url_host='sponge-http.appspot.com', 914 upload_url_path='/create_invocation', 915 update_url_path='/update_target_result', 916 server=None): 917 """Initializes the object. 918 919 Args: 920 url_host: host or host:port for the Sponge HTTP Redirector server. 921 upload_url_path: the path after url_host. 922 update_url_path: the path after update_url_host. 923 server: name of the Sponge backend, if None use SERVER_QA. 924 """ 925 super(Uploader, self).__init__() 926 self.server = server or SERVER_QA 927 self.invocations = [] 928 self._url_host = url_host 929 self._upload_url_path = upload_url_path 930 self._update_url_path = update_url_path 931 self._proxy = None 932 self._https_connection_factory = httplib.HTTPSConnection 933 934 def WriteXml(self, xml_writer): 935 """Writes this object as XML suitable for Sponge HTTP Redirector. 936 937 Args: 938 xml_writer: google3.third_party.python.elementtree.SimpleXMLWriter. 939 """ 940 xml_writer.start('xml') 941 super(Uploader, self).WriteXml(xml_writer) 942 xml_writer.end() 943 944 def UseProxy(self, proxy): 945 """Forward requests through a given HTTP proxy. 946 947 Args: 948 proxy: the proxy address as '<host>' or '<host>:<port>' 949 """ 950 self._proxy = proxy 951 952 def UseHTTPSConnectionFactory(self, https_connection_factory): 953 """Use the given function to create HTTPS connections. 954 955 This is helpful for clients on later version of Python (2.7.9+) that wish to 956 do client-side SSL authentication via ssl.SSLContext. 957 958 Args: 959 https_connection_factory: A function that takes a string url and returns 960 an httplib.HTTPSConnection. 961 """ 962 self._https_connection_factory = https_connection_factory 963 964 def Upload(self): 965 """Uploads Sponge invocations to the Sponge HTTP Redirector service. 966 967 Returns: 968 A string with Sponge invocation IDs, as returned by the HTTP Redirector. 969 970 Raises: 971 ValueError: when at least one invocation id is not None. 972 """ 973 for invocation in self.invocations: 974 if invocation.id: 975 raise ValueError( 976 'Invocation id must be None for new invocation.') 977 return self._UploadHelper(self._url_host, self._upload_url_path) 978 979 def UploadUpdatedResults(self): 980 """Uploads updated Sponge invocations to the Sponge HTTP Redirector service. 981 982 Returns: 983 A string with Sponge invocation IDs, as returned by the HTTP Redirector. 984 985 Raises: 986 ValueError: when at least one invocation id is None or at least one 987 target result has index of None. 988 """ 989 for invocation in self.invocations: 990 if invocation.id is None: 991 raise ValueError('Invocation id must not be None for update.') 992 for target_result in invocation.target_results: 993 if target_result.index is None: 994 raise ValueError( 995 'Target result index can not be None for update.') 996 return self._UploadHelper(self._url_host, self._update_url_path) 997 998 def _UploadHelper(self, host, url): 999 """A helper function to perform actual upload of Sponge invocations. 1000 1001 Args: 1002 host: host server to connect to. 1003 url: url for Sponge end point. 1004 1005 Returns: 1006 A string represent Sponge invocation IDs. 1007 """ 1008 if self._proxy: 1009 # A simple HTTP proxy request is the same as a regular HTTP request 1010 # via the proxy host:port, except the path after the method (GET or POST) 1011 # is the full actual request URL. 1012 url = 'https://%s%s' % (host, url) 1013 # Assume proxy does not support HTTPS. 1014 http_connect = httplib.HTTPConnection(self._proxy) 1015 else: 1016 http_connect = self._https_connection_factory(host) 1017 xml_str = self.GetXmlString() 1018 http_connect.connect() 1019 http_connect.request('PUT', url, body=xml_str) 1020 response = http_connect.getresponse() 1021 response_str = response.read().strip() 1022 if response_str.startswith('id: "'): 1023 response_str = response_str[5:-1] 1024 return response_str 1025 1026 1027def GetInvocationUrl(server, invocation_id): 1028 if server == 'backend-qa': 1029 return 'http://sponge-qa/%s' % invocation_id 1030 else: 1031 return 'http://tests/%s' % invocation_id 1032