diff options
Diffstat (limited to 'src/third_party/wiredtiger/test/3rdparty/python-subunit-0.0.16/python/subunit/v2.py')
-rw-r--r-- | src/third_party/wiredtiger/test/3rdparty/python-subunit-0.0.16/python/subunit/v2.py | 495 |
1 files changed, 495 insertions, 0 deletions
diff --git a/src/third_party/wiredtiger/test/3rdparty/python-subunit-0.0.16/python/subunit/v2.py b/src/third_party/wiredtiger/test/3rdparty/python-subunit-0.0.16/python/subunit/v2.py new file mode 100644 index 00000000000..057f65c3bdd --- /dev/null +++ b/src/third_party/wiredtiger/test/3rdparty/python-subunit-0.0.16/python/subunit/v2.py @@ -0,0 +1,495 @@ +# +# subunit: extensions to Python unittest to get test results from subprocesses. +# Copyright (C) 2013 Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. +# + +import codecs +utf_8_decode = codecs.utf_8_decode +import datetime +from io import UnsupportedOperation +import os +import select +import struct +import zlib + +from extras import safe_hasattr, try_imports +builtins = try_imports(['__builtin__', 'builtins']) + +import subunit +import subunit.iso8601 as iso8601 + +__all__ = [ + 'ByteStreamToStreamResult', + 'StreamResultToBytes', + ] + +SIGNATURE = b'\xb3' +FMT_8 = '>B' +FMT_16 = '>H' +FMT_24 = '>HB' +FMT_32 = '>I' +FMT_TIMESTAMP = '>II' +FLAG_TEST_ID = 0x0800 +FLAG_ROUTE_CODE = 0x0400 +FLAG_TIMESTAMP = 0x0200 +FLAG_RUNNABLE = 0x0100 +FLAG_TAGS = 0x0080 +FLAG_MIME_TYPE = 0x0020 +FLAG_EOF = 0x0010 +FLAG_FILE_CONTENT = 0x0040 +EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=iso8601.Utc()) +NUL_ELEMENT = b'\0'[0] +# Contains True for types for which 'nul in thing' falsely returns false. +_nul_test_broken = {} + + +def has_nul(buffer_or_bytes): + """Return True if a null byte is present in buffer_or_bytes.""" + # Simple "if NUL_ELEMENT in utf8_bytes:" fails on Python 3.1 and 3.2 with + # memoryviews. See https://bugs.launchpad.net/subunit/+bug/1216246 + buffer_type = type(buffer_or_bytes) + broken = _nul_test_broken.get(buffer_type) + if broken is None: + reference = buffer_type(b'\0') + broken = not NUL_ELEMENT in reference + _nul_test_broken[buffer_type] = broken + if broken: + return b'\0' in buffer_or_bytes + else: + return NUL_ELEMENT in buffer_or_bytes + + +class ParseError(Exception): + """Used to pass error messages within the parser.""" + + +class StreamResultToBytes(object): + """Convert StreamResult API calls to bytes. + + The StreamResult API is defined by testtools.StreamResult. + """ + + status_mask = { + None: 0, + 'exists': 0x1, + 'inprogress': 0x2, + 'success': 0x3, + 'uxsuccess': 0x4, + 'skip': 0x5, + 'fail': 0x6, + 'xfail': 0x7, + } + + zero_b = b'\0'[0] + + def __init__(self, output_stream): + """Create a StreamResultToBytes with output written to output_stream. + + :param output_stream: A file-like object. Must support write(bytes) + and flush() methods. Flush will be called after each write. + The stream will be passed through subunit.make_stream_binary, + to handle regular cases such as stdout. + """ + self.output_stream = subunit.make_stream_binary(output_stream) + + def startTestRun(self): + pass + + def stopTestRun(self): + pass + + def status(self, test_id=None, test_status=None, test_tags=None, + runnable=True, file_name=None, file_bytes=None, eof=False, + mime_type=None, route_code=None, timestamp=None): + self._write_packet(test_id=test_id, test_status=test_status, + test_tags=test_tags, runnable=runnable, file_name=file_name, + file_bytes=file_bytes, eof=eof, mime_type=mime_type, + route_code=route_code, timestamp=timestamp) + + def _write_utf8(self, a_string, packet): + utf8 = a_string.encode('utf-8') + self._write_number(len(utf8), packet) + packet.append(utf8) + + def _write_len16(self, length, packet): + assert length < 65536 + packet.append(struct.pack(FMT_16, length)) + + def _write_number(self, value, packet): + packet.extend(self._encode_number(value)) + + def _encode_number(self, value): + assert value >= 0 + if value < 64: + return [struct.pack(FMT_8, value)] + elif value < 16384: + value = value | 0x4000 + return [struct.pack(FMT_16, value)] + elif value < 4194304: + value = value | 0x800000 + return [struct.pack(FMT_16, value >> 8), + struct.pack(FMT_8, value & 0xff)] + elif value < 1073741824: + value = value | 0xc0000000 + return [struct.pack(FMT_32, value)] + else: + raise ValueError('value too large to encode: %r' % (value,)) + + def _write_packet(self, test_id=None, test_status=None, test_tags=None, + runnable=True, file_name=None, file_bytes=None, eof=False, + mime_type=None, route_code=None, timestamp=None): + packet = [SIGNATURE] + packet.append(b'FF') # placeholder for flags + # placeholder for length, but see below as length is variable. + packet.append(b'') + flags = 0x2000 # Version 0x2 + if timestamp is not None: + flags = flags | FLAG_TIMESTAMP + since_epoch = timestamp - EPOCH + nanoseconds = since_epoch.microseconds * 1000 + seconds = (since_epoch.seconds + since_epoch.days * 24 * 3600) + packet.append(struct.pack(FMT_32, seconds)) + self._write_number(nanoseconds, packet) + if test_id is not None: + flags = flags | FLAG_TEST_ID + self._write_utf8(test_id, packet) + if test_tags: + flags = flags | FLAG_TAGS + self._write_number(len(test_tags), packet) + for tag in test_tags: + self._write_utf8(tag, packet) + if runnable: + flags = flags | FLAG_RUNNABLE + if mime_type: + flags = flags | FLAG_MIME_TYPE + self._write_utf8(mime_type, packet) + if file_name is not None: + flags = flags | FLAG_FILE_CONTENT + self._write_utf8(file_name, packet) + self._write_number(len(file_bytes), packet) + packet.append(file_bytes) + if eof: + flags = flags | FLAG_EOF + if route_code is not None: + flags = flags | FLAG_ROUTE_CODE + self._write_utf8(route_code, packet) + # 0x0008 - not used in v2. + flags = flags | self.status_mask[test_status] + packet[1] = struct.pack(FMT_16, flags) + base_length = sum(map(len, packet)) + 4 + if base_length <= 62: + # one byte to encode length, 62+1 = 63 + length_length = 1 + elif base_length <= 16381: + # two bytes to encode length, 16381+2 = 16383 + length_length = 2 + elif base_length <= 4194300: + # three bytes to encode length, 419430+3=4194303 + length_length = 3 + else: + # Longer than policy: + # TODO: chunk the packet automatically? + # - strip all but file data + # - do 4M chunks of that till done + # - include original data in final chunk. + raise ValueError("Length too long: %r" % base_length) + packet[2:3] = self._encode_number(base_length + length_length) + # We could either do a partial application of crc32 over each chunk + # or a single join to a temp variable then a final join + # or two writes (that python might then split). + # For now, simplest code: join, crc32, join, output + content = b''.join(packet) + self.output_stream.write(content + struct.pack( + FMT_32, zlib.crc32(content) & 0xffffffff)) + self.output_stream.flush() + + +class ByteStreamToStreamResult(object): + """Parse a subunit byte stream. + + Mixed streams that contain non-subunit content is supported when a + non_subunit_name is passed to the contructor. The default is to raise an + error containing the non-subunit byte after it has been read from the + stream. + + Typical use: + + >>> case = ByteStreamToStreamResult(sys.stdin.buffer) + >>> result = StreamResult() + >>> result.startTestRun() + >>> case.run(result) + >>> result.stopTestRun() + """ + + status_lookup = { + 0x0: None, + 0x1: 'exists', + 0x2: 'inprogress', + 0x3: 'success', + 0x4: 'uxsuccess', + 0x5: 'skip', + 0x6: 'fail', + 0x7: 'xfail', + } + + def __init__(self, source, non_subunit_name=None): + """Create a ByteStreamToStreamResult. + + :param source: A file like object to read bytes from. Must support + read(<count>) and return bytes. The file is not closed by + ByteStreamToStreamResult. subunit.make_stream_binary() is + called on the stream to get it into bytes mode. + :param non_subunit_name: If set to non-None, non subunit content + encountered in the stream will be converted into file packets + labelled with this name. + """ + self.non_subunit_name = non_subunit_name + self.source = subunit.make_stream_binary(source) + self.codec = codecs.lookup('utf8').incrementaldecoder() + + def run(self, result): + """Parse source and emit events to result. + + This is a blocking call: it will run until EOF is detected on source. + """ + self.codec.reset() + mid_character = False + while True: + # We're in blocking mode; read one char + content = self.source.read(1) + if not content: + # EOF + return + if not mid_character and content[0] == SIGNATURE[0]: + self._parse_packet(result) + continue + if self.non_subunit_name is None: + raise Exception("Non subunit content", content) + try: + if self.codec.decode(content): + # End of a character + mid_character = False + else: + mid_character = True + except UnicodeDecodeError: + # Bad unicode, not our concern. + mid_character = False + # Aggregate all content that is not subunit until either + # 1MiB is accumulated or 50ms has passed with no input. + # Both are arbitrary amounts intended to give a simple + # balance between efficiency (avoiding death by a thousand + # one-byte packets), buffering (avoiding overlarge state + # being hidden on intermediary nodes) and interactivity + # (when driving a debugger, slow response to typing is + # annoying). + buffered = [content] + while len(buffered[-1]): + try: + self.source.fileno() + except: + # Won't be able to select, fallback to + # one-byte-at-a-time. + break + # Note: this has a very low timeout because with stdin, the + # BufferedIO layer typically has all the content available + # from the stream when e.g. pdb is dropped into, leading to + # select always timing out when in fact we could have read + # (from the buffer layer) - we typically fail to aggregate + # any content on 3.x Pythons. + readable = select.select([self.source], [], [], 0.000001)[0] + if readable: + content = self.source.read(1) + if not len(content): + # EOF - break and emit buffered. + break + if not mid_character and content[0] == SIGNATURE[0]: + # New packet, break, emit buffered, then parse. + break + buffered.append(content) + # Feed into the codec. + try: + if self.codec.decode(content): + # End of a character + mid_character = False + else: + mid_character = True + except UnicodeDecodeError: + # Bad unicode, not our concern. + mid_character = False + if not readable or len(buffered) >= 1048576: + # timeout or too much data, emit what we have. + break + result.status( + file_name=self.non_subunit_name, + file_bytes=b''.join(buffered)) + if mid_character or not len(content) or content[0] != SIGNATURE[0]: + continue + # Otherwise, parse a data packet. + self._parse_packet(result) + + def _parse_packet(self, result): + try: + packet = [SIGNATURE] + self._parse(packet, result) + except ParseError as error: + result.status(test_id="subunit.parser", eof=True, + file_name="Packet data", file_bytes=b''.join(packet), + mime_type="application/octet-stream") + result.status(test_id="subunit.parser", test_status='fail', + eof=True, file_name="Parser Error", + file_bytes=(error.args[0]).encode('utf8'), + mime_type="text/plain;charset=utf8") + + def _to_bytes(self, data, pos, length): + """Return a slice of data from pos for length as bytes.""" + # memoryview in 2.7.3 and 3.2 isn't directly usable with struct :(. + # see https://bugs.launchpad.net/subunit/+bug/1216163 + result = data[pos:pos+length] + if type(result) is not bytes: + return result.tobytes() + return result + + def _parse_varint(self, data, pos, max_3_bytes=False): + # because the only incremental IO we do is at the start, and the 32 bit + # CRC means we can always safely read enough to cover any varint, we + # can be sure that there should be enough data - and if not it is an + # error not a normal situation. + data_0 = struct.unpack(FMT_8, self._to_bytes(data, pos, 1))[0] + typeenum = data_0 & 0xc0 + value_0 = data_0 & 0x3f + if typeenum == 0x00: + return value_0, 1 + elif typeenum == 0x40: + data_1 = struct.unpack(FMT_8, self._to_bytes(data, pos+1, 1))[0] + return (value_0 << 8) | data_1, 2 + elif typeenum == 0x80: + data_1 = struct.unpack(FMT_16, self._to_bytes(data, pos+1, 2))[0] + return (value_0 << 16) | data_1, 3 + else: + if max_3_bytes: + raise ParseError('3 byte maximum given but 4 byte value found.') + data_1, data_2 = struct.unpack(FMT_24, self._to_bytes(data, pos+1, 3)) + result = (value_0 << 24) | data_1 << 8 | data_2 + return result, 4 + + def _parse(self, packet, result): + # 2 bytes flags, at most 3 bytes length. + packet.append(self.source.read(5)) + flags = struct.unpack(FMT_16, packet[-1][:2])[0] + length, consumed = self._parse_varint( + packet[-1], 2, max_3_bytes=True) + remainder = self.source.read(length - 6) + if len(remainder) != length - 6: + raise ParseError( + 'Short read - got %d bytes, wanted %d bytes' % ( + len(remainder), length - 6)) + if consumed != 3: + # Avoid having to parse torn values + packet[-1] += remainder + pos = 2 + consumed + else: + # Avoid copying potentially lots of data. + packet.append(remainder) + pos = 0 + crc = zlib.crc32(packet[0]) + for fragment in packet[1:-1]: + crc = zlib.crc32(fragment, crc) + crc = zlib.crc32(packet[-1][:-4], crc) & 0xffffffff + packet_crc = struct.unpack(FMT_32, packet[-1][-4:])[0] + if crc != packet_crc: + # Bad CRC, report it and stop parsing the packet. + raise ParseError( + 'Bad checksum - calculated (0x%x), stored (0x%x)' + % (crc, packet_crc)) + if safe_hasattr(builtins, 'memoryview'): + body = memoryview(packet[-1]) + else: + body = packet[-1] + # Discard CRC-32 + body = body[:-4] + # One packet could have both file and status data; the Python API + # presents these separately (perhaps it shouldn't?) + if flags & FLAG_TIMESTAMP: + seconds = struct.unpack(FMT_32, self._to_bytes(body, pos, 4))[0] + nanoseconds, consumed = self._parse_varint(body, pos+4) + pos = pos + 4 + consumed + timestamp = EPOCH + datetime.timedelta( + seconds=seconds, microseconds=nanoseconds/1000) + else: + timestamp = None + if flags & FLAG_TEST_ID: + test_id, pos = self._read_utf8(body, pos) + else: + test_id = None + if flags & FLAG_TAGS: + tag_count, consumed = self._parse_varint(body, pos) + pos += consumed + test_tags = set() + for _ in range(tag_count): + tag, pos = self._read_utf8(body, pos) + test_tags.add(tag) + else: + test_tags = None + if flags & FLAG_MIME_TYPE: + mime_type, pos = self._read_utf8(body, pos) + else: + mime_type = None + if flags & FLAG_FILE_CONTENT: + file_name, pos = self._read_utf8(body, pos) + content_length, consumed = self._parse_varint(body, pos) + pos += consumed + file_bytes = self._to_bytes(body, pos, content_length) + if len(file_bytes) != content_length: + raise ParseError('File content extends past end of packet: ' + 'claimed %d bytes, %d available' % ( + content_length, len(file_bytes))) + pos += content_length + else: + file_name = None + file_bytes = None + if flags & FLAG_ROUTE_CODE: + route_code, pos = self._read_utf8(body, pos) + else: + route_code = None + runnable = bool(flags & FLAG_RUNNABLE) + eof = bool(flags & FLAG_EOF) + test_status = self.status_lookup[flags & 0x0007] + result.status(test_id=test_id, test_status=test_status, + test_tags=test_tags, runnable=runnable, mime_type=mime_type, + eof=eof, file_name=file_name, file_bytes=file_bytes, + route_code=route_code, timestamp=timestamp) + __call__ = run + + def _read_utf8(self, buf, pos): + length, consumed = self._parse_varint(buf, pos) + pos += consumed + utf8_bytes = buf[pos:pos+length] + if length != len(utf8_bytes): + raise ParseError( + 'UTF8 string at offset %d extends past end of packet: ' + 'claimed %d bytes, %d available' % (pos - 2, length, + len(utf8_bytes))) + if has_nul(utf8_bytes): + raise ParseError('UTF8 string at offset %d contains NUL byte' % ( + pos-2,)) + try: + utf8, decoded_bytes = utf_8_decode(utf8_bytes) + if decoded_bytes != length: + raise ParseError("Invalid (partially decodable) string at " + "offset %d, %d undecoded bytes" % ( + pos-2, length - decoded_bytes)) + return utf8, length+pos + except UnicodeDecodeError: + raise ParseError('UTF8 string at offset %d is not UTF8' % (pos-2,)) + |