Update test/3rdparty with the packages required to run the test suite in parallel mode. Change the short command line flag to "-j", matching make.

--HG--
rename : test/3rdparty/testscenarios-0.2/.bzrignore => test/3rdparty/testscenarios-0.4/.bzrignore
rename : test/3rdparty/testscenarios-0.2/Apache-2.0 => test/3rdparty/testscenarios-0.4/Apache-2.0
rename : test/3rdparty/testscenarios-0.2/BSD => test/3rdparty/testscenarios-0.4/BSD
rename : test/3rdparty/testscenarios-0.2/COPYING => test/3rdparty/testscenarios-0.4/COPYING
rename : test/3rdparty/testscenarios-0.2/GOALS => test/3rdparty/testscenarios-0.4/GOALS
rename : test/3rdparty/testscenarios-0.2/HACKING => test/3rdparty/testscenarios-0.4/HACKING
rename : test/3rdparty/testscenarios-0.2/MANIFEST.in => test/3rdparty/testscenarios-0.4/MANIFEST.in
rename : test/3rdparty/testscenarios-0.2/Makefile => test/3rdparty/testscenarios-0.4/Makefile
rename : test/3rdparty/testscenarios-0.2/doc/__init__.py => test/3rdparty/testscenarios-0.4/doc/__init__.py
rename : test/3rdparty/testscenarios-0.2/doc/example.py => test/3rdparty/testscenarios-0.4/doc/example.py
rename : test/3rdparty/testscenarios-0.2/doc/test_sample.py => test/3rdparty/testscenarios-0.4/doc/test_sample.py
rename : test/3rdparty/testtools-0.9.12/doc/conf.py => test/3rdparty/testtools-0.9.34/doc/conf.py
rename : test/3rdparty/testtools-0.9.12/doc/make.bat => test/3rdparty/testtools-0.9.34/doc/make.bat
rename : test/3rdparty/testtools-0.9.12/testtools/_compat2x.py => test/3rdparty/testtools-0.9.34/testtools/_compat2x.py
rename : test/3rdparty/testtools-0.9.12/testtools/_spinner.py => test/3rdparty/testtools-0.9.34/testtools/_spinner.py
rename : test/3rdparty/testtools-0.9.12/testtools/distutilscmd.py => test/3rdparty/testtools-0.9.34/testtools/distutilscmd.py
rename : test/3rdparty/testtools-0.9.12/testtools/monkey.py => test/3rdparty/testtools-0.9.34/testtools/monkey.py
rename : test/3rdparty/testtools-0.9.12/testtools/tests/test_monkey.py => test/3rdparty/testtools-0.9.34/testtools/tests/test_monkey.py
rename : test/3rdparty/testtools-0.9.12/testtools/tests/test_runtest.py => test/3rdparty/testtools-0.9.34/testtools/tests/test_runtest.py
rename : test/3rdparty/testtools-0.9.12/testtools/utils.py => test/3rdparty/testtools-0.9.34/testtools/utils.py
This commit is contained in:
Michael Cahill
2013-12-20 10:27:28 +11:00
parent e2fcf1a851
commit fa69e2a994
194 changed files with 24285 additions and 5494 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,185 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net>
# Copyright (C) 2011 Martin Pool <mbp@sourcefrog.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.
#
"""Encoder/decoder for http style chunked encoding."""
from testtools.compat import _b
empty = _b('')
class Decoder(object):
"""Decode chunked content to a byte stream."""
def __init__(self, output, strict=True):
"""Create a decoder decoding to output.
:param output: A file-like object. Bytes written to the Decoder are
decoded to strip off the chunking and written to the output.
Up to a full write worth of data or a single control line may be
buffered (whichever is larger). The close method should be called
when no more data is available, to detect short streams; the
write method will return none-None when the end of a stream is
detected. The output object must accept bytes objects.
:param strict: If True (the default), the decoder will not knowingly
accept input that is not conformant to the HTTP specification.
(This does not imply that it will catch every nonconformance.)
If False, it will accept incorrect input that is still
unambiguous.
"""
self.output = output
self.buffered_bytes = []
self.state = self._read_length
self.body_length = 0
self.strict = strict
self._match_chars = _b("0123456789abcdefABCDEF\r\n")
self._slash_n = _b('\n')
self._slash_r = _b('\r')
self._slash_rn = _b('\r\n')
self._slash_nr = _b('\n\r')
def close(self):
"""Close the decoder.
:raises ValueError: If the stream is incomplete ValueError is raised.
"""
if self.state != self._finished:
raise ValueError("incomplete stream")
def _finished(self):
"""Finished reading, return any remaining bytes."""
if self.buffered_bytes:
buffered_bytes = self.buffered_bytes
self.buffered_bytes = []
return empty.join(buffered_bytes)
else:
raise ValueError("stream is finished")
def _read_body(self):
"""Pass body bytes to the output."""
while self.body_length and self.buffered_bytes:
if self.body_length >= len(self.buffered_bytes[0]):
self.output.write(self.buffered_bytes[0])
self.body_length -= len(self.buffered_bytes[0])
del self.buffered_bytes[0]
# No more data available.
if not self.body_length:
self.state = self._read_length
else:
self.output.write(self.buffered_bytes[0][:self.body_length])
self.buffered_bytes[0] = \
self.buffered_bytes[0][self.body_length:]
self.body_length = 0
self.state = self._read_length
return self.state()
def _read_length(self):
"""Try to decode a length from the bytes."""
count_chars = []
for bytes in self.buffered_bytes:
for pos in range(len(bytes)):
byte = bytes[pos:pos+1]
if byte not in self._match_chars:
break
count_chars.append(byte)
if byte == self._slash_n:
break
if not count_chars:
return
if count_chars[-1] != self._slash_n:
return
count_str = empty.join(count_chars)
if self.strict:
if count_str[-2:] != self._slash_rn:
raise ValueError("chunk header invalid: %r" % count_str)
if self._slash_r in count_str[:-2]:
raise ValueError("too many CRs in chunk header %r" % count_str)
self.body_length = int(count_str.rstrip(self._slash_nr), 16)
excess_bytes = len(count_str)
while excess_bytes:
if excess_bytes >= len(self.buffered_bytes[0]):
excess_bytes -= len(self.buffered_bytes[0])
del self.buffered_bytes[0]
else:
self.buffered_bytes[0] = self.buffered_bytes[0][excess_bytes:]
excess_bytes = 0
if not self.body_length:
self.state = self._finished
if not self.buffered_bytes:
# May not call into self._finished with no buffered data.
return empty
else:
self.state = self._read_body
return self.state()
def write(self, bytes):
"""Decode bytes to the output stream.
:raises ValueError: If the stream has already seen the end of file
marker.
:returns: None, or the excess bytes beyond the end of file marker.
"""
if bytes:
self.buffered_bytes.append(bytes)
return self.state()
class Encoder(object):
"""Encode content to a stream using HTTP Chunked coding."""
def __init__(self, output):
"""Create an encoder encoding to output.
:param output: A file-like object. Bytes written to the Encoder
will be encoded using HTTP chunking. Small writes may be buffered
and the ``close`` method must be called to finish the stream.
"""
self.output = output
self.buffered_bytes = []
self.buffer_size = 0
def flush(self, extra_len=0):
"""Flush the encoder to the output stream.
:param extra_len: Increase the size of the chunk by this many bytes
to allow for a subsequent write.
"""
if not self.buffer_size and not extra_len:
return
buffered_bytes = self.buffered_bytes
buffer_size = self.buffer_size
self.buffered_bytes = []
self.buffer_size = 0
self.output.write(_b("%X\r\n" % (buffer_size + extra_len)))
if buffer_size:
self.output.write(empty.join(buffered_bytes))
return True
def write(self, bytes):
"""Encode bytes to the output stream."""
bytes_len = len(bytes)
if self.buffer_size + bytes_len >= 65536:
self.flush(bytes_len)
self.output.write(bytes)
else:
self.buffered_bytes.append(bytes)
self.buffer_size += bytes_len
def close(self):
"""Finish the stream. This does not close the output stream."""
self.flush()
self.output.write(_b("0\r\n"))

View File

@@ -0,0 +1,119 @@
#
# subunit: extensions to Python unittest to get test results from subprocesses.
# Copyright (C) 2005 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.
#
"""Handlers for outcome details."""
from testtools import content, content_type
from testtools.compat import _b, BytesIO
from subunit import chunked
end_marker = _b("]\n")
quoted_marker = _b(" ]")
empty = _b('')
class DetailsParser(object):
"""Base class/API reference for details parsing."""
class SimpleDetailsParser(DetailsParser):
"""Parser for single-part [] delimited details."""
def __init__(self, state):
self._message = _b("")
self._state = state
def lineReceived(self, line):
if line == end_marker:
self._state.endDetails()
return
if line[0:2] == quoted_marker:
# quoted ] start
self._message += line[1:]
else:
self._message += line
def get_details(self, style=None):
result = {}
if not style:
# We know that subunit/testtools serialise [] formatted
# tracebacks as utf8, but perhaps we need a ReplacingContent
# or something like that.
result['traceback'] = content.Content(
content_type.ContentType("text", "x-traceback",
{"charset": "utf8"}),
lambda:[self._message])
else:
if style == 'skip':
name = 'reason'
else:
name = 'message'
result[name] = content.Content(
content_type.ContentType("text", "plain"),
lambda:[self._message])
return result
def get_message(self):
return self._message
class MultipartDetailsParser(DetailsParser):
"""Parser for multi-part [] surrounded MIME typed chunked details."""
def __init__(self, state):
self._state = state
self._details = {}
self._parse_state = self._look_for_content
def _look_for_content(self, line):
if line == end_marker:
self._state.endDetails()
return
# TODO error handling
field, value = line[:-1].decode('utf8').split(' ', 1)
try:
main, sub = value.split('/')
except ValueError:
raise ValueError("Invalid MIME type %r" % value)
self._content_type = content_type.ContentType(main, sub)
self._parse_state = self._get_name
def _get_name(self, line):
self._name = line[:-1].decode('utf8')
self._body = BytesIO()
self._chunk_parser = chunked.Decoder(self._body)
self._parse_state = self._feed_chunks
def _feed_chunks(self, line):
residue = self._chunk_parser.write(line)
if residue is not None:
# Line based use always ends on no residue.
assert residue == empty, 'residue: %r' % (residue,)
body = self._body
self._details[self._name] = content.Content(
self._content_type, lambda:[body.getvalue()])
self._chunk_parser.close()
self._parse_state = self._look_for_content
def get_details(self, for_skip=False):
return self._details
def get_message(self):
return None
def lineReceived(self, line):
self._parse_state(line)

View File

@@ -0,0 +1,206 @@
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2009 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.
#
from optparse import OptionParser
import sys
from extras import safe_hasattr
from testtools import CopyStreamResult, StreamResult, StreamResultRouter
from subunit import (
DiscardStream, ProtocolTestCase, ByteStreamToStreamResult,
StreamResultToBytes,
)
from subunit.test_results import CatFiles
def make_options(description):
parser = OptionParser(description=description)
parser.add_option(
"--no-passthrough", action="store_true",
help="Hide all non subunit input.", default=False,
dest="no_passthrough")
parser.add_option(
"-o", "--output-to",
help="Send the output to this path rather than stdout.")
parser.add_option(
"-f", "--forward", action="store_true", default=False,
help="Forward subunit stream on stdout. When set, received "
"non-subunit output will be encapsulated in subunit.")
return parser
def run_tests_from_stream(input_stream, result, passthrough_stream=None,
forward_stream=None, protocol_version=1, passthrough_subunit=True):
"""Run tests from a subunit input stream through 'result'.
Non-test events - top level file attachments - are expected to be
dropped by v2 StreamResults at the present time (as all the analysis code
is in ExtendedTestResult API's), so to implement passthrough_stream they
are diverted and copied directly when that is set.
:param input_stream: A stream containing subunit input.
:param result: A TestResult that will receive the test events.
NB: This should be an ExtendedTestResult for v1 and a StreamResult for
v2.
:param passthrough_stream: All non-subunit input received will be
sent to this stream. If not provided, uses the ``TestProtocolServer``
default, which is ``sys.stdout``.
:param forward_stream: All subunit input received will be forwarded
to this stream. If not provided, uses the ``TestProtocolServer``
default, which is to not forward any input. Do not set this when
transforming the stream - items would be double-reported.
:param protocol_version: What version of the subunit protocol to expect.
:param passthrough_subunit: If True, passthrough should be as subunit
otherwise unwrap it. Only has effect when forward_stream is None.
(when forwarding as subunit non-subunit input is always turned into
subunit)
"""
if 1==protocol_version:
test = ProtocolTestCase(
input_stream, passthrough=passthrough_stream,
forward=forward_stream)
elif 2==protocol_version:
# In all cases we encapsulate unknown inputs.
if forward_stream is not None:
# Send events to forward_stream as subunit.
forward_result = StreamResultToBytes(forward_stream)
# If we're passing non-subunit through, copy:
if passthrough_stream is None:
# Not passing non-test events - split them off to nothing.
router = StreamResultRouter(forward_result)
router.add_rule(StreamResult(), 'test_id', test_id=None)
result = CopyStreamResult([router, result])
else:
# otherwise, copy all events to forward_result
result = CopyStreamResult([forward_result, result])
elif passthrough_stream is not None:
if not passthrough_subunit:
# Route non-test events to passthrough_stream, unwrapping them for
# display.
passthrough_result = CatFiles(passthrough_stream)
else:
passthrough_result = StreamResultToBytes(passthrough_stream)
result = StreamResultRouter(result)
result.add_rule(passthrough_result, 'test_id', test_id=None)
test = ByteStreamToStreamResult(input_stream,
non_subunit_name='stdout')
else:
raise Exception("Unknown protocol version.")
result.startTestRun()
test.run(result)
result.stopTestRun()
def filter_by_result(result_factory, output_path, passthrough, forward,
input_stream=sys.stdin, protocol_version=1,
passthrough_subunit=True):
"""Filter an input stream using a test result.
:param result_factory: A callable that when passed an output stream
returns a TestResult. It is expected that this result will output
to the given stream.
:param output_path: A path send output to. If None, output will be go
to ``sys.stdout``.
:param passthrough: If True, all non-subunit input will be sent to
``sys.stdout``. If False, that input will be discarded.
:param forward: If True, all subunit input will be forwarded directly to
``sys.stdout`` as well as to the ``TestResult``.
:param input_stream: The source of subunit input. Defaults to
``sys.stdin``.
:param protocol_version: The subunit protocol version to expect.
:param passthrough_subunit: If True, passthrough should be as subunit.
:return: A test result with the results of the run.
"""
if passthrough:
passthrough_stream = sys.stdout
else:
if 1==protocol_version:
passthrough_stream = DiscardStream()
else:
passthrough_stream = None
if forward:
forward_stream = sys.stdout
elif 1==protocol_version:
forward_stream = DiscardStream()
else:
forward_stream = None
if output_path is None:
output_to = sys.stdout
else:
output_to = file(output_path, 'wb')
try:
result = result_factory(output_to)
run_tests_from_stream(
input_stream, result, passthrough_stream, forward_stream,
protocol_version=protocol_version,
passthrough_subunit=passthrough_subunit)
finally:
if output_path:
output_to.close()
return result
def run_filter_script(result_factory, description, post_run_hook=None,
protocol_version=1, passthrough_subunit=True):
"""Main function for simple subunit filter scripts.
Many subunit filter scripts take a stream of subunit input and use a
TestResult to handle the events generated by that stream. This function
wraps a lot of the boiler-plate around that by making a script with
options for handling passthrough information and stream forwarding, and
that will exit with a successful return code (i.e. 0) if the input stream
represents a successful test run.
:param result_factory: A callable that takes an output stream and returns
a test result that outputs to that stream.
:param description: A description of the filter script.
:param protocol_version: What protocol version to consume/emit.
:param passthrough_subunit: If True, passthrough should be as subunit.
"""
parser = make_options(description)
(options, args) = parser.parse_args()
result = filter_by_result(
result_factory, options.output_to, not options.no_passthrough,
options.forward, protocol_version=protocol_version,
passthrough_subunit=passthrough_subunit,
input_stream=find_stream(sys.stdin, args))
if post_run_hook:
post_run_hook(result)
if not safe_hasattr(result, 'wasSuccessful'):
result = result.decorated
if result.wasSuccessful():
sys.exit(0)
else:
sys.exit(1)
def find_stream(stdin, argv):
"""Find a stream to use as input for filters.
:param stdin: Standard in - used if no files are named in argv.
:param argv: Command line arguments after option parsing. If one file
is named, that is opened in read only binary mode and returned.
A missing file will raise an exception, as will multiple file names.
"""
assert len(argv) < 2, "Too many filenames."
if argv:
return open(argv[0], 'rb')
else:
return stdin

View File

@@ -0,0 +1,133 @@
# Copyright (c) 2007 Michael Twomey
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""ISO 8601 date time string parsing
Basic usage:
>>> import iso8601
>>> iso8601.parse_date("2007-01-25T12:00:00Z")
datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
>>>
"""
from datetime import datetime, timedelta, tzinfo
import re
import sys
__all__ = ["parse_date", "ParseError"]
# Adapted from http://delete.me.uk/2005/03/iso8601.html
ISO8601_REGEX_PATTERN = (r"(?P<year>[0-9]{4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
r"((?P<separator>.)(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?)?"
r"(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"
)
TIMEZONE_REGEX_PATTERN = "(?P<prefix>[+-])(?P<hours>[0-9]{2}).(?P<minutes>[0-9]{2})"
ISO8601_REGEX = re.compile(ISO8601_REGEX_PATTERN.encode('utf8'))
TIMEZONE_REGEX = re.compile(TIMEZONE_REGEX_PATTERN.encode('utf8'))
zulu = "Z".encode('latin-1')
minus = "-".encode('latin-1')
if sys.version_info < (3, 0):
bytes = str
class ParseError(Exception):
"""Raised when there is a problem parsing a date string"""
# Yoinked from python docs
ZERO = timedelta(0)
class Utc(tzinfo):
"""UTC
"""
def utcoffset(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return ZERO
UTC = Utc()
class FixedOffset(tzinfo):
"""Fixed offset in hours and minutes from UTC
"""
def __init__(self, offset_hours, offset_minutes, name):
self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
self.__name = name
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return ZERO
def __repr__(self):
return "<FixedOffset %r>" % self.__name
def parse_timezone(tzstring, default_timezone=UTC):
"""Parses ISO 8601 time zone specs into tzinfo offsets
"""
if tzstring == zulu:
return default_timezone
# This isn't strictly correct, but it's common to encounter dates without
# timezones so I'll assume the default (which defaults to UTC).
# Addresses issue 4.
if tzstring is None:
return default_timezone
m = TIMEZONE_REGEX.match(tzstring)
prefix, hours, minutes = m.groups()
hours, minutes = int(hours), int(minutes)
if prefix == minus:
hours = -hours
minutes = -minutes
return FixedOffset(hours, minutes, tzstring)
def parse_date(datestring, default_timezone=UTC):
"""Parses ISO 8601 dates into datetime objects
The timezone is parsed from the date string. However it is quite common to
have dates without a timezone (not strictly correct). In this case the
default timezone specified in default_timezone is used. This is UTC by
default.
"""
if not isinstance(datestring, bytes):
raise ParseError("Expecting bytes %r" % datestring)
m = ISO8601_REGEX.match(datestring)
if not m:
raise ParseError("Unable to parse date string %r" % datestring)
groups = m.groupdict()
tz = parse_timezone(groups["timezone"], default_timezone=default_timezone)
if groups["fraction"] is None:
groups["fraction"] = 0
else:
groups["fraction"] = int(float("0.%s" % groups["fraction"].decode()) * 1e6)
return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]),
int(groups["hour"]), int(groups["minute"]), int(groups["second"]),
int(groups["fraction"]), tz)

View File

@@ -0,0 +1,106 @@
#
# subunit: extensions to Python unittest to get test results from subprocesses.
# Copyright (C) 2009 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.
#
"""Support for dealing with progress state."""
class ProgressModel(object):
"""A model of progress indicators as subunit defines it.
Instances of this class represent a single logical operation that is
progressing. The operation may have many steps, and some of those steps may
supply their own progress information. ProgressModel uses a nested concept
where the overall state can be pushed, creating new starting state, and
later pushed to return to the prior state. Many user interfaces will want
to display an overall summary though, and accordingly the pos() and width()
methods return overall summary information rather than information on the
current subtask.
The default state is 0/0 - indicating that the overall progress is unknown.
Anytime the denominator of pos/width is 0, rendering of a ProgressModel
should should take this into consideration.
:ivar: _tasks. This private attribute stores the subtasks. Each is a tuple:
pos, width, overall_numerator, overall_denominator. The overall fields
store the calculated overall numerator and denominator for the state
that was pushed.
"""
def __init__(self):
"""Create a ProgressModel.
The new model has no progress data at all - it will claim a summary
width of zero and position of 0.
"""
self._tasks = []
self.push()
def adjust_width(self, offset):
"""Adjust the with of the current subtask."""
self._tasks[-1][1] += offset
def advance(self):
"""Advance the current subtask."""
self._tasks[-1][0] += 1
def pop(self):
"""Pop a subtask off the ProgressModel.
See push for a description of how push and pop work.
"""
self._tasks.pop()
def pos(self):
"""Return how far through the operation has progressed."""
if not self._tasks:
return 0
task = self._tasks[-1]
if len(self._tasks) > 1:
# scale up the overall pos by the current task or preserve it if
# no current width is known.
offset = task[2] * (task[1] or 1)
else:
offset = 0
return offset + task[0]
def push(self):
"""Push a new subtask.
After pushing a new subtask, the overall progress hasn't changed. Calls
to adjust_width, advance, set_width will only after the progress within
the range that calling 'advance' would have before - the subtask
represents progressing one step in the earlier task.
Call pop() to restore the progress model to the state before push was
called.
"""
self._tasks.append([0, 0, self.pos(), self.width()])
def set_width(self, width):
"""Set the width of the current subtask."""
self._tasks[-1][1] = width
def width(self):
"""Return the total width of the operation."""
if not self._tasks:
return 0
task = self._tasks[-1]
if len(self._tasks) > 1:
# scale up the overall width by the current task or preserve it if
# no current width is known.
return task[3] * (task[1] or 1)
else:
return task[1]

View File

@@ -0,0 +1,131 @@
#!/usr/bin/python
#
# Simple subunit testrunner for python
# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007
#
# 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.
#
"""Run a unittest testcase reporting results as Subunit.
$ python -m subunit.run mylib.tests.test_suite
"""
import io
import os
import sys
from testtools import ExtendedToStreamDecorator
from testtools.testsuite import iterate_tests
from subunit import StreamResultToBytes, get_default_formatter
from subunit.test_results import AutoTimingTestResultDecorator
from testtools.run import (
BUFFEROUTPUT,
CATCHBREAK,
FAILFAST,
list_test,
TestProgram,
USAGE_AS_MAIN,
)
class SubunitTestRunner(object):
def __init__(self, verbosity=None, failfast=None, buffer=None, stream=None):
"""Create a TestToolsTestRunner.
:param verbosity: Ignored.
:param failfast: Stop running tests at the first failure.
:param buffer: Ignored.
"""
self.failfast = failfast
self.stream = stream or sys.stdout
def run(self, test):
"Run the given test case or test suite."
result, _ = self._list(test)
result = ExtendedToStreamDecorator(result)
result = AutoTimingTestResultDecorator(result)
if self.failfast is not None:
result.failfast = self.failfast
result.startTestRun()
try:
test(result)
finally:
result.stopTestRun()
return result
def list(self, test):
"List the test."
result, errors = self._list(test)
if errors:
failed_descr = '\n'.join(errors).encode('utf8')
result.status(file_name="import errors", runnable=False,
file_bytes=failed_descr, mime_type="text/plain;charset=utf8")
sys.exit(2)
def _list(self, test):
test_ids, errors = list_test(test)
try:
fileno = self.stream.fileno()
except:
fileno = None
if fileno is not None:
stream = os.fdopen(fileno, 'wb', 0)
else:
stream = self.stream
result = StreamResultToBytes(stream)
for test_id in test_ids:
result.status(test_id=test_id, test_status='exists')
return result, errors
class SubunitTestProgram(TestProgram):
USAGE = USAGE_AS_MAIN
def usageExit(self, msg=None):
if msg:
print (msg)
usage = {'progName': self.progName, 'catchbreak': '', 'failfast': '',
'buffer': ''}
if self.failfast != False:
usage['failfast'] = FAILFAST
if self.catchbreak != False:
usage['catchbreak'] = CATCHBREAK
if self.buffer != False:
usage['buffer'] = BUFFEROUTPUT
usage_text = self.USAGE % usage
usage_lines = usage_text.split('\n')
usage_lines.insert(2, "Run a test suite with a subunit reporter.")
usage_lines.insert(3, "")
print('\n'.join(usage_lines))
sys.exit(2)
def main():
# Disable the default buffering, for Python 2.x where pdb doesn't do it
# on non-ttys.
stream = get_default_formatter()
runner = SubunitTestRunner
# Patch stdout to be unbuffered, so that pdb works well on 2.6/2.7.
binstdout = io.open(sys.stdout.fileno(), 'wb', 0)
if sys.version_info[0] > 2:
sys.stdout = io.TextIOWrapper(binstdout, encoding=sys.stdout.encoding)
else:
sys.stdout = binstdout
SubunitTestProgram(module=None, argv=sys.argv, testRunner=runner,
stdout=sys.stdout)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,729 @@
#
# subunit: extensions to Python unittest to get test results from subprocesses.
# Copyright (C) 2009 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.
#
"""TestResult helper classes used to by subunit."""
import csv
import datetime
import testtools
from testtools.compat import all
from testtools.content import (
text_content,
TracebackContent,
)
from testtools import StreamResult
from subunit import iso8601
import subunit
# NOT a TestResult, because we are implementing the interface, not inheriting
# it.
class TestResultDecorator(object):
"""General pass-through decorator.
This provides a base that other TestResults can inherit from to
gain basic forwarding functionality. It also takes care of
handling the case where the target doesn't support newer methods
or features by degrading them.
"""
# XXX: Since lp:testtools r250, this is in testtools. Once it's released,
# we should gut this and just use that.
def __init__(self, decorated):
"""Create a TestResultDecorator forwarding to decorated."""
# Make every decorator degrade gracefully.
self.decorated = testtools.ExtendedToOriginalDecorator(decorated)
def startTest(self, test):
return self.decorated.startTest(test)
def startTestRun(self):
return self.decorated.startTestRun()
def stopTest(self, test):
return self.decorated.stopTest(test)
def stopTestRun(self):
return self.decorated.stopTestRun()
def addError(self, test, err=None, details=None):
return self.decorated.addError(test, err, details=details)
def addFailure(self, test, err=None, details=None):
return self.decorated.addFailure(test, err, details=details)
def addSuccess(self, test, details=None):
return self.decorated.addSuccess(test, details=details)
def addSkip(self, test, reason=None, details=None):
return self.decorated.addSkip(test, reason, details=details)
def addExpectedFailure(self, test, err=None, details=None):
return self.decorated.addExpectedFailure(test, err, details=details)
def addUnexpectedSuccess(self, test, details=None):
return self.decorated.addUnexpectedSuccess(test, details=details)
def _get_failfast(self):
return getattr(self.decorated, 'failfast', False)
def _set_failfast(self, value):
self.decorated.failfast = value
failfast = property(_get_failfast, _set_failfast)
def progress(self, offset, whence):
return self.decorated.progress(offset, whence)
def wasSuccessful(self):
return self.decorated.wasSuccessful()
@property
def shouldStop(self):
return self.decorated.shouldStop
def stop(self):
return self.decorated.stop()
@property
def testsRun(self):
return self.decorated.testsRun
def tags(self, new_tags, gone_tags):
return self.decorated.tags(new_tags, gone_tags)
def time(self, a_datetime):
return self.decorated.time(a_datetime)
class HookedTestResultDecorator(TestResultDecorator):
"""A TestResult which calls a hook on every event."""
def __init__(self, decorated):
self.super = super(HookedTestResultDecorator, self)
self.super.__init__(decorated)
def startTest(self, test):
self._before_event()
return self.super.startTest(test)
def startTestRun(self):
self._before_event()
return self.super.startTestRun()
def stopTest(self, test):
self._before_event()
return self.super.stopTest(test)
def stopTestRun(self):
self._before_event()
return self.super.stopTestRun()
def addError(self, test, err=None, details=None):
self._before_event()
return self.super.addError(test, err, details=details)
def addFailure(self, test, err=None, details=None):
self._before_event()
return self.super.addFailure(test, err, details=details)
def addSuccess(self, test, details=None):
self._before_event()
return self.super.addSuccess(test, details=details)
def addSkip(self, test, reason=None, details=None):
self._before_event()
return self.super.addSkip(test, reason, details=details)
def addExpectedFailure(self, test, err=None, details=None):
self._before_event()
return self.super.addExpectedFailure(test, err, details=details)
def addUnexpectedSuccess(self, test, details=None):
self._before_event()
return self.super.addUnexpectedSuccess(test, details=details)
def progress(self, offset, whence):
self._before_event()
return self.super.progress(offset, whence)
def wasSuccessful(self):
self._before_event()
return self.super.wasSuccessful()
@property
def shouldStop(self):
self._before_event()
return self.super.shouldStop
def stop(self):
self._before_event()
return self.super.stop()
def time(self, a_datetime):
self._before_event()
return self.super.time(a_datetime)
class AutoTimingTestResultDecorator(HookedTestResultDecorator):
"""Decorate a TestResult to add time events to a test run.
By default this will cause a time event before every test event,
but if explicit time data is being provided by the test run, then
this decorator will turn itself off to prevent causing confusion.
"""
def __init__(self, decorated):
self._time = None
super(AutoTimingTestResultDecorator, self).__init__(decorated)
def _before_event(self):
time = self._time
if time is not None:
return
time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc())
self.decorated.time(time)
def progress(self, offset, whence):
return self.decorated.progress(offset, whence)
@property
def shouldStop(self):
return self.decorated.shouldStop
def time(self, a_datetime):
"""Provide a timestamp for the current test activity.
:param a_datetime: If None, automatically add timestamps before every
event (this is the default behaviour if time() is not called at
all). If not None, pass the provided time onto the decorated
result object and disable automatic timestamps.
"""
self._time = a_datetime
return self.decorated.time(a_datetime)
class TagsMixin(object):
def __init__(self):
self._clear_tags()
def _clear_tags(self):
self._global_tags = set(), set()
self._test_tags = None
def _get_active_tags(self):
global_new, global_gone = self._global_tags
if self._test_tags is None:
return set(global_new)
test_new, test_gone = self._test_tags
return global_new.difference(test_gone).union(test_new)
def _get_current_scope(self):
if self._test_tags:
return self._test_tags
return self._global_tags
def _flush_current_scope(self, tag_receiver):
new_tags, gone_tags = self._get_current_scope()
if new_tags or gone_tags:
tag_receiver.tags(new_tags, gone_tags)
if self._test_tags:
self._test_tags = set(), set()
else:
self._global_tags = set(), set()
def startTestRun(self):
self._clear_tags()
def startTest(self, test):
self._test_tags = set(), set()
def stopTest(self, test):
self._test_tags = None
def tags(self, new_tags, gone_tags):
"""Handle tag instructions.
Adds and removes tags as appropriate. If a test is currently running,
tags are not affected for subsequent tests.
:param new_tags: Tags to add,
:param gone_tags: Tags to remove.
"""
current_new_tags, current_gone_tags = self._get_current_scope()
current_new_tags.update(new_tags)
current_new_tags.difference_update(gone_tags)
current_gone_tags.update(gone_tags)
current_gone_tags.difference_update(new_tags)
class TagCollapsingDecorator(HookedTestResultDecorator, TagsMixin):
"""Collapses many 'tags' calls into one where possible."""
def __init__(self, result):
super(TagCollapsingDecorator, self).__init__(result)
self._clear_tags()
def _before_event(self):
self._flush_current_scope(self.decorated)
def tags(self, new_tags, gone_tags):
TagsMixin.tags(self, new_tags, gone_tags)
class TimeCollapsingDecorator(HookedTestResultDecorator):
"""Only pass on the first and last of a consecutive sequence of times."""
def __init__(self, decorated):
super(TimeCollapsingDecorator, self).__init__(decorated)
self._last_received_time = None
self._last_sent_time = None
def _before_event(self):
if self._last_received_time is None:
return
if self._last_received_time != self._last_sent_time:
self.decorated.time(self._last_received_time)
self._last_sent_time = self._last_received_time
self._last_received_time = None
def time(self, a_time):
# Don't upcall, because we don't want to call _before_event, it's only
# for non-time events.
if self._last_received_time is None:
self.decorated.time(a_time)
self._last_sent_time = a_time
self._last_received_time = a_time
def and_predicates(predicates):
"""Return a predicate that is true iff all predicates are true."""
# XXX: Should probably be in testtools to be better used by matchers. jml
return lambda *args, **kwargs: all(p(*args, **kwargs) for p in predicates)
def make_tag_filter(with_tags, without_tags):
"""Make a callback that checks tests against tags."""
with_tags = with_tags and set(with_tags) or None
without_tags = without_tags and set(without_tags) or None
def check_tags(test, outcome, err, details, tags):
if with_tags and not with_tags <= tags:
return False
if without_tags and bool(without_tags & tags):
return False
return True
return check_tags
class _PredicateFilter(TestResultDecorator, TagsMixin):
def __init__(self, result, predicate):
super(_PredicateFilter, self).__init__(result)
self._clear_tags()
self.decorated = TimeCollapsingDecorator(
TagCollapsingDecorator(self.decorated))
self._predicate = predicate
# The current test (for filtering tags)
self._current_test = None
# Has the current test been filtered (for outputting test tags)
self._current_test_filtered = None
# Calls to this result that we don't know whether to forward on yet.
self._buffered_calls = []
def filter_predicate(self, test, outcome, error, details):
return self._predicate(
test, outcome, error, details, self._get_active_tags())
def addError(self, test, err=None, details=None):
if (self.filter_predicate(test, 'error', err, details)):
self._buffered_calls.append(
('addError', [test, err], {'details': details}))
else:
self._filtered()
def addFailure(self, test, err=None, details=None):
if (self.filter_predicate(test, 'failure', err, details)):
self._buffered_calls.append(
('addFailure', [test, err], {'details': details}))
else:
self._filtered()
def addSkip(self, test, reason=None, details=None):
if (self.filter_predicate(test, 'skip', reason, details)):
self._buffered_calls.append(
('addSkip', [test, reason], {'details': details}))
else:
self._filtered()
def addExpectedFailure(self, test, err=None, details=None):
if self.filter_predicate(test, 'expectedfailure', err, details):
self._buffered_calls.append(
('addExpectedFailure', [test, err], {'details': details}))
else:
self._filtered()
def addUnexpectedSuccess(self, test, details=None):
self._buffered_calls.append(
('addUnexpectedSuccess', [test], {'details': details}))
def addSuccess(self, test, details=None):
if (self.filter_predicate(test, 'success', None, details)):
self._buffered_calls.append(
('addSuccess', [test], {'details': details}))
else:
self._filtered()
def _filtered(self):
self._current_test_filtered = True
def startTest(self, test):
"""Start a test.
Not directly passed to the client, but used for handling of tags
correctly.
"""
TagsMixin.startTest(self, test)
self._current_test = test
self._current_test_filtered = False
self._buffered_calls.append(('startTest', [test], {}))
def stopTest(self, test):
"""Stop a test.
Not directly passed to the client, but used for handling of tags
correctly.
"""
if not self._current_test_filtered:
for method, args, kwargs in self._buffered_calls:
getattr(self.decorated, method)(*args, **kwargs)
self.decorated.stopTest(test)
self._current_test = None
self._current_test_filtered = None
self._buffered_calls = []
TagsMixin.stopTest(self, test)
def tags(self, new_tags, gone_tags):
TagsMixin.tags(self, new_tags, gone_tags)
if self._current_test is not None:
self._buffered_calls.append(('tags', [new_tags, gone_tags], {}))
else:
return super(_PredicateFilter, self).tags(new_tags, gone_tags)
def time(self, a_time):
return self.decorated.time(a_time)
def id_to_orig_id(self, id):
if id.startswith("subunit.RemotedTestCase."):
return id[len("subunit.RemotedTestCase."):]
return id
class TestResultFilter(TestResultDecorator):
"""A pyunit TestResult interface implementation which filters tests.
Tests that pass the filter are handed on to another TestResult instance
for further processing/reporting. To obtain the filtered results,
the other instance must be interrogated.
:ivar result: The result that tests are passed to after filtering.
:ivar filter_predicate: The callback run to decide whether to pass
a result.
"""
def __init__(self, result, filter_error=False, filter_failure=False,
filter_success=True, filter_skip=False, filter_xfail=False,
filter_predicate=None, fixup_expected_failures=None):
"""Create a FilterResult object filtering to result.
:param filter_error: Filter out errors.
:param filter_failure: Filter out failures.
:param filter_success: Filter out successful tests.
:param filter_skip: Filter out skipped tests.
:param filter_xfail: Filter out expected failure tests.
:param filter_predicate: A callable taking (test, outcome, err,
details, tags) and returning True if the result should be passed
through. err and details may be none if no error or extra
metadata is available. outcome is the name of the outcome such
as 'success' or 'failure'. tags is new in 0.0.8; 0.0.7 filters
are still supported but should be updated to accept the tags
parameter for efficiency.
:param fixup_expected_failures: Set of test ids to consider known
failing.
"""
predicates = []
if filter_error:
predicates.append(
lambda t, outcome, e, d, tags: outcome != 'error')
if filter_failure:
predicates.append(
lambda t, outcome, e, d, tags: outcome != 'failure')
if filter_success:
predicates.append(
lambda t, outcome, e, d, tags: outcome != 'success')
if filter_skip:
predicates.append(
lambda t, outcome, e, d, tags: outcome != 'skip')
if filter_xfail:
predicates.append(
lambda t, outcome, e, d, tags: outcome != 'expectedfailure')
if filter_predicate is not None:
def compat(test, outcome, error, details, tags):
# 0.0.7 and earlier did not support the 'tags' parameter.
try:
return filter_predicate(
test, outcome, error, details, tags)
except TypeError:
return filter_predicate(test, outcome, error, details)
predicates.append(compat)
predicate = and_predicates(predicates)
super(TestResultFilter, self).__init__(
_PredicateFilter(result, predicate))
if fixup_expected_failures is None:
self._fixup_expected_failures = frozenset()
else:
self._fixup_expected_failures = fixup_expected_failures
def addError(self, test, err=None, details=None):
if self._failure_expected(test):
self.addExpectedFailure(test, err=err, details=details)
else:
super(TestResultFilter, self).addError(
test, err=err, details=details)
def addFailure(self, test, err=None, details=None):
if self._failure_expected(test):
self.addExpectedFailure(test, err=err, details=details)
else:
super(TestResultFilter, self).addFailure(
test, err=err, details=details)
def addSuccess(self, test, details=None):
if self._failure_expected(test):
self.addUnexpectedSuccess(test, details=details)
else:
super(TestResultFilter, self).addSuccess(test, details=details)
def _failure_expected(self, test):
return (test.id() in self._fixup_expected_failures)
class TestIdPrintingResult(testtools.TestResult):
"""Print test ids to a stream.
Implements both TestResult and StreamResult, for compatibility.
"""
def __init__(self, stream, show_times=False, show_exists=False):
"""Create a FilterResult object outputting to stream."""
super(TestIdPrintingResult, self).__init__()
self._stream = stream
self.show_exists = show_exists
self.show_times = show_times
def startTestRun(self):
self.failed_tests = 0
self.__time = None
self._test = None
self._test_duration = 0
self._active_tests = {}
def addError(self, test, err):
self.failed_tests += 1
self._test = test
def addFailure(self, test, err):
self.failed_tests += 1
self._test = test
def addSuccess(self, test):
self._test = test
def addSkip(self, test, reason=None, details=None):
self._test = test
def addUnexpectedSuccess(self, test, details=None):
self.failed_tests += 1
self._test = test
def addExpectedFailure(self, test, err=None, details=None):
self._test = test
def reportTest(self, test_id, duration):
if self.show_times:
seconds = duration.seconds
seconds += duration.days * 3600 * 24
seconds += duration.microseconds / 1000000.0
self._stream.write(test_id + ' %0.3f\n' % seconds)
else:
self._stream.write(test_id + '\n')
def startTest(self, test):
self._start_time = self._time()
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):
if not test_id:
return
if timestamp is not None:
self.time(timestamp)
if test_status=='exists':
if self.show_exists:
self.reportTest(test_id, 0)
elif test_status in ('inprogress', None):
self._active_tests[test_id] = self._time()
else:
self._end_test(test_id)
def _end_test(self, test_id):
test_start = self._active_tests.pop(test_id, None)
if not test_start:
test_duration = 0
else:
test_duration = self._time() - test_start
self.reportTest(test_id, test_duration)
def stopTest(self, test):
test_duration = self._time() - self._start_time
self.reportTest(self._test.id(), test_duration)
def time(self, time):
self.__time = time
def _time(self):
return self.__time
def wasSuccessful(self):
"Tells whether or not this result was a success"
return self.failed_tests == 0
def stopTestRun(self):
for test_id in list(self._active_tests.keys()):
self._end_test(test_id)
class TestByTestResult(testtools.TestResult):
"""Call something every time a test completes."""
# XXX: In testtools since lp:testtools r249. Once that's released, just
# import that.
def __init__(self, on_test):
"""Construct a ``TestByTestResult``.
:param on_test: A callable that take a test case, a status (one of
"success", "failure", "error", "skip", or "xfail"), a start time
(a ``datetime`` with timezone), a stop time, an iterable of tags,
and a details dict. Is called at the end of each test (i.e. on
``stopTest``) with the accumulated values for that test.
"""
super(TestByTestResult, self).__init__()
self._on_test = on_test
def startTest(self, test):
super(TestByTestResult, self).startTest(test)
self._start_time = self._now()
# There's no supported (i.e. tested) behaviour that relies on these
# being set, but it makes me more comfortable all the same. -- jml
self._status = None
self._details = None
self._stop_time = None
def stopTest(self, test):
self._stop_time = self._now()
super(TestByTestResult, self).stopTest(test)
self._on_test(
test=test,
status=self._status,
start_time=self._start_time,
stop_time=self._stop_time,
# current_tags is new in testtools 0.9.13.
tags=getattr(self, 'current_tags', None),
details=self._details)
def _err_to_details(self, test, err, details):
if details:
return details
return {'traceback': TracebackContent(err, test)}
def addSuccess(self, test, details=None):
super(TestByTestResult, self).addSuccess(test)
self._status = 'success'
self._details = details
def addFailure(self, test, err=None, details=None):
super(TestByTestResult, self).addFailure(test, err, details)
self._status = 'failure'
self._details = self._err_to_details(test, err, details)
def addError(self, test, err=None, details=None):
super(TestByTestResult, self).addError(test, err, details)
self._status = 'error'
self._details = self._err_to_details(test, err, details)
def addSkip(self, test, reason=None, details=None):
super(TestByTestResult, self).addSkip(test, reason, details)
self._status = 'skip'
if details is None:
details = {'reason': text_content(reason)}
elif reason:
# XXX: What if details already has 'reason' key?
details['reason'] = text_content(reason)
self._details = details
def addExpectedFailure(self, test, err=None, details=None):
super(TestByTestResult, self).addExpectedFailure(test, err, details)
self._status = 'xfail'
self._details = self._err_to_details(test, err, details)
def addUnexpectedSuccess(self, test, details=None):
super(TestByTestResult, self).addUnexpectedSuccess(test, details)
self._status = 'success'
self._details = details
class CsvResult(TestByTestResult):
def __init__(self, stream):
super(CsvResult, self).__init__(self._on_test)
self._write_row = csv.writer(stream).writerow
def _on_test(self, test, status, start_time, stop_time, tags, details):
self._write_row([test.id(), status, start_time, stop_time])
def startTestRun(self):
super(CsvResult, self).startTestRun()
self._write_row(['test', 'status', 'start_time', 'stop_time'])
class CatFiles(StreamResult):
"""Cat file attachments received to a stream."""
def __init__(self, byte_stream):
self.stream = subunit.make_stream_binary(byte_stream)
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):
if file_name is not None:
self.stream.write(file_bytes)
self.stream.flush()

View File

@@ -0,0 +1,63 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 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 sys
from unittest import TestLoader
# Before the test module imports to avoid circularity.
# For testing: different pythons have different str() implementations.
if sys.version_info > (3, 0):
_remote_exception_repr = "testtools.testresult.real._StringException"
_remote_exception_str = "Traceback (most recent call last):\ntesttools.testresult.real._StringException"
_remote_exception_str_chunked = "57\r\n" + _remote_exception_str + ": boo qux\n0\r\n"
else:
_remote_exception_repr = "_StringException"
_remote_exception_str = "Traceback (most recent call last):\n_StringException"
_remote_exception_str_chunked = "3D\r\n" + _remote_exception_str + ": boo qux\n0\r\n"
from subunit.tests import (
test_chunked,
test_details,
test_filters,
test_progress_model,
test_run,
test_subunit_filter,
test_subunit_stats,
test_subunit_tags,
test_tap2subunit,
test_test_protocol,
test_test_protocol2,
test_test_results,
)
def test_suite():
loader = TestLoader()
result = loader.loadTestsFromModule(test_chunked)
result.addTest(loader.loadTestsFromModule(test_details))
result.addTest(loader.loadTestsFromModule(test_filters))
result.addTest(loader.loadTestsFromModule(test_progress_model))
result.addTest(loader.loadTestsFromModule(test_test_results))
result.addTest(loader.loadTestsFromModule(test_test_protocol))
result.addTest(loader.loadTestsFromModule(test_test_protocol2))
result.addTest(loader.loadTestsFromModule(test_tap2subunit))
result.addTest(loader.loadTestsFromModule(test_subunit_filter))
result.addTest(loader.loadTestsFromModule(test_subunit_tags))
result.addTest(loader.loadTestsFromModule(test_subunit_stats))
result.addTest(loader.loadTestsFromModule(test_run))
return result

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python
import sys
if sys.platform == "win32":
import msvcrt, os
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
if len(sys.argv) == 2:
# subunit.tests.test_test_protocol.TestExecTestCase.test_sample_method_args
# uses this code path to be sure that the arguments were passed to
# sample-script.py
print("test fail")
print("error fail")
sys.exit(0)
print("test old mcdonald")
print("success old mcdonald")
print("test bing crosby")
print("failure bing crosby [")
print("foo.c:53:ERROR invalid state")
print("]")
print("test an error")
print("error an error")
sys.exit(0)

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env python
import sys
print("test old mcdonald")
print("success old mcdonald")
print("test bing crosby")
print("success bing crosby")
sys.exit(0)

View File

@@ -0,0 +1,146 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net>
# Copyright (C) 2011 Martin Pool <mbp@sourcefrog.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 unittest
from testtools.compat import _b, BytesIO
import subunit.chunked
class TestDecode(unittest.TestCase):
def setUp(self):
unittest.TestCase.setUp(self)
self.output = BytesIO()
self.decoder = subunit.chunked.Decoder(self.output)
def test_close_read_length_short_errors(self):
self.assertRaises(ValueError, self.decoder.close)
def test_close_body_short_errors(self):
self.assertEqual(None, self.decoder.write(_b('2\r\na')))
self.assertRaises(ValueError, self.decoder.close)
def test_close_body_buffered_data_errors(self):
self.assertEqual(None, self.decoder.write(_b('2\r')))
self.assertRaises(ValueError, self.decoder.close)
def test_close_after_finished_stream_safe(self):
self.assertEqual(None, self.decoder.write(_b('2\r\nab')))
self.assertEqual(_b(''), self.decoder.write(_b('0\r\n')))
self.decoder.close()
def test_decode_nothing(self):
self.assertEqual(_b(''), self.decoder.write(_b('0\r\n')))
self.assertEqual(_b(''), self.output.getvalue())
def test_decode_serialised_form(self):
self.assertEqual(None, self.decoder.write(_b("F\r\n")))
self.assertEqual(None, self.decoder.write(_b("serialised\n")))
self.assertEqual(_b(''), self.decoder.write(_b("form0\r\n")))
def test_decode_short(self):
self.assertEqual(_b(''), self.decoder.write(_b('3\r\nabc0\r\n')))
self.assertEqual(_b('abc'), self.output.getvalue())
def test_decode_combines_short(self):
self.assertEqual(_b(''), self.decoder.write(_b('6\r\nabcdef0\r\n')))
self.assertEqual(_b('abcdef'), self.output.getvalue())
def test_decode_excess_bytes_from_write(self):
self.assertEqual(_b('1234'), self.decoder.write(_b('3\r\nabc0\r\n1234')))
self.assertEqual(_b('abc'), self.output.getvalue())
def test_decode_write_after_finished_errors(self):
self.assertEqual(_b('1234'), self.decoder.write(_b('3\r\nabc0\r\n1234')))
self.assertRaises(ValueError, self.decoder.write, _b(''))
def test_decode_hex(self):
self.assertEqual(_b(''), self.decoder.write(_b('A\r\n12345678900\r\n')))
self.assertEqual(_b('1234567890'), self.output.getvalue())
def test_decode_long_ranges(self):
self.assertEqual(None, self.decoder.write(_b('10000\r\n')))
self.assertEqual(None, self.decoder.write(_b('1' * 65536)))
self.assertEqual(None, self.decoder.write(_b('10000\r\n')))
self.assertEqual(None, self.decoder.write(_b('2' * 65536)))
self.assertEqual(_b(''), self.decoder.write(_b('0\r\n')))
self.assertEqual(_b('1' * 65536 + '2' * 65536), self.output.getvalue())
def test_decode_newline_nonstrict(self):
"""Tolerate chunk markers with no CR character."""
# From <http://pad.lv/505078>
self.decoder = subunit.chunked.Decoder(self.output, strict=False)
self.assertEqual(None, self.decoder.write(_b('a\n')))
self.assertEqual(None, self.decoder.write(_b('abcdeabcde')))
self.assertEqual(_b(''), self.decoder.write(_b('0\n')))
self.assertEqual(_b('abcdeabcde'), self.output.getvalue())
def test_decode_strict_newline_only(self):
"""Reject chunk markers with no CR character in strict mode."""
# From <http://pad.lv/505078>
self.assertRaises(ValueError,
self.decoder.write, _b('a\n'))
def test_decode_strict_multiple_crs(self):
self.assertRaises(ValueError,
self.decoder.write, _b('a\r\r\n'))
def test_decode_short_header(self):
self.assertRaises(ValueError,
self.decoder.write, _b('\n'))
class TestEncode(unittest.TestCase):
def setUp(self):
unittest.TestCase.setUp(self)
self.output = BytesIO()
self.encoder = subunit.chunked.Encoder(self.output)
def test_encode_nothing(self):
self.encoder.close()
self.assertEqual(_b('0\r\n'), self.output.getvalue())
def test_encode_empty(self):
self.encoder.write(_b(''))
self.encoder.close()
self.assertEqual(_b('0\r\n'), self.output.getvalue())
def test_encode_short(self):
self.encoder.write(_b('abc'))
self.encoder.close()
self.assertEqual(_b('3\r\nabc0\r\n'), self.output.getvalue())
def test_encode_combines_short(self):
self.encoder.write(_b('abc'))
self.encoder.write(_b('def'))
self.encoder.close()
self.assertEqual(_b('6\r\nabcdef0\r\n'), self.output.getvalue())
def test_encode_over_9_is_in_hex(self):
self.encoder.write(_b('1234567890'))
self.encoder.close()
self.assertEqual(_b('A\r\n12345678900\r\n'), self.output.getvalue())
def test_encode_long_ranges_not_combined(self):
self.encoder.write(_b('1' * 65536))
self.encoder.write(_b('2' * 65536))
self.encoder.close()
self.assertEqual(_b('10000\r\n' + '1' * 65536 + '10000\r\n' +
'2' * 65536 + '0\r\n'), self.output.getvalue())

View File

@@ -0,0 +1,106 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 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 unittest
from testtools.compat import _b, StringIO
import subunit.tests
from subunit import content, content_type, details
class TestSimpleDetails(unittest.TestCase):
def test_lineReceived(self):
parser = details.SimpleDetailsParser(None)
parser.lineReceived(_b("foo\n"))
parser.lineReceived(_b("bar\n"))
self.assertEqual(_b("foo\nbar\n"), parser._message)
def test_lineReceived_escaped_bracket(self):
parser = details.SimpleDetailsParser(None)
parser.lineReceived(_b("foo\n"))
parser.lineReceived(_b(" ]are\n"))
parser.lineReceived(_b("bar\n"))
self.assertEqual(_b("foo\n]are\nbar\n"), parser._message)
def test_get_message(self):
parser = details.SimpleDetailsParser(None)
self.assertEqual(_b(""), parser.get_message())
def test_get_details(self):
parser = details.SimpleDetailsParser(None)
traceback = ""
expected = {}
expected['traceback'] = content.Content(
content_type.ContentType("text", "x-traceback",
{'charset': 'utf8'}),
lambda:[_b("")])
found = parser.get_details()
self.assertEqual(expected.keys(), found.keys())
self.assertEqual(expected['traceback'].content_type,
found['traceback'].content_type)
self.assertEqual(_b('').join(expected['traceback'].iter_bytes()),
_b('').join(found['traceback'].iter_bytes()))
def test_get_details_skip(self):
parser = details.SimpleDetailsParser(None)
traceback = ""
expected = {}
expected['reason'] = content.Content(
content_type.ContentType("text", "plain"),
lambda:[_b("")])
found = parser.get_details("skip")
self.assertEqual(expected, found)
def test_get_details_success(self):
parser = details.SimpleDetailsParser(None)
traceback = ""
expected = {}
expected['message'] = content.Content(
content_type.ContentType("text", "plain"),
lambda:[_b("")])
found = parser.get_details("success")
self.assertEqual(expected, found)
class TestMultipartDetails(unittest.TestCase):
def test_get_message_is_None(self):
parser = details.MultipartDetailsParser(None)
self.assertEqual(None, parser.get_message())
def test_get_details(self):
parser = details.MultipartDetailsParser(None)
self.assertEqual({}, parser.get_details())
def test_parts(self):
parser = details.MultipartDetailsParser(None)
parser.lineReceived(_b("Content-Type: text/plain\n"))
parser.lineReceived(_b("something\n"))
parser.lineReceived(_b("F\r\n"))
parser.lineReceived(_b("serialised\n"))
parser.lineReceived(_b("form0\r\n"))
expected = {}
expected['something'] = content.Content(
content_type.ContentType("text", "plain"),
lambda:[_b("serialised\nform")])
found = parser.get_details()
self.assertEqual(expected.keys(), found.keys())
self.assertEqual(expected['something'].content_type,
found['something'].content_type)
self.assertEqual(_b('').join(expected['something'].iter_bytes()),
_b('').join(found['something'].iter_bytes()))

View File

@@ -0,0 +1,35 @@
#
# 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 sys
from tempfile import NamedTemporaryFile
from testtools import TestCase
from subunit.filters import find_stream
class TestFindStream(TestCase):
def test_no_argv(self):
self.assertEqual('foo', find_stream('foo', []))
def test_opens_file(self):
f = NamedTemporaryFile()
f.write(b'foo')
f.flush()
stream = find_stream('bar', [f.name])
self.assertEqual(b'foo', stream.read())

View File

@@ -0,0 +1,112 @@
#
# subunit: extensions to Python unittest to get test results from subprocesses.
# Copyright (C) 2009 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 unittest
import subunit
from subunit.progress_model import ProgressModel
class TestProgressModel(unittest.TestCase):
def assertProgressSummary(self, pos, total, progress):
"""Assert that a progress model has reached a particular point."""
self.assertEqual(pos, progress.pos())
self.assertEqual(total, progress.width())
def test_new_progress_0_0(self):
progress = ProgressModel()
self.assertProgressSummary(0, 0, progress)
def test_advance_0_0(self):
progress = ProgressModel()
progress.advance()
self.assertProgressSummary(1, 0, progress)
def test_advance_1_0(self):
progress = ProgressModel()
progress.advance()
self.assertProgressSummary(1, 0, progress)
def test_set_width_absolute(self):
progress = ProgressModel()
progress.set_width(10)
self.assertProgressSummary(0, 10, progress)
def test_set_width_absolute_preserves_pos(self):
progress = ProgressModel()
progress.advance()
progress.set_width(2)
self.assertProgressSummary(1, 2, progress)
def test_adjust_width(self):
progress = ProgressModel()
progress.adjust_width(10)
self.assertProgressSummary(0, 10, progress)
progress.adjust_width(-10)
self.assertProgressSummary(0, 0, progress)
def test_adjust_width_preserves_pos(self):
progress = ProgressModel()
progress.advance()
progress.adjust_width(10)
self.assertProgressSummary(1, 10, progress)
progress.adjust_width(-10)
self.assertProgressSummary(1, 0, progress)
def test_push_preserves_progress(self):
progress = ProgressModel()
progress.adjust_width(3)
progress.advance()
progress.push()
self.assertProgressSummary(1, 3, progress)
def test_advance_advances_substack(self):
progress = ProgressModel()
progress.adjust_width(3)
progress.advance()
progress.push()
progress.adjust_width(1)
progress.advance()
self.assertProgressSummary(2, 3, progress)
def test_adjust_width_adjusts_substack(self):
progress = ProgressModel()
progress.adjust_width(3)
progress.advance()
progress.push()
progress.adjust_width(2)
progress.advance()
self.assertProgressSummary(3, 6, progress)
def test_set_width_adjusts_substack(self):
progress = ProgressModel()
progress.adjust_width(3)
progress.advance()
progress.push()
progress.set_width(2)
progress.advance()
self.assertProgressSummary(3, 6, progress)
def test_pop_restores_progress(self):
progress = ProgressModel()
progress.adjust_width(3)
progress.advance()
progress.push()
progress.adjust_width(1)
progress.advance()
progress.pop()
self.assertProgressSummary(1, 3, progress)

View File

@@ -0,0 +1,64 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2011 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.
#
from testtools.compat import BytesIO
import unittest
from testtools import PlaceHolder, TestCase
from testtools.testresult.doubles import StreamResult
import subunit
from subunit import run
from subunit.run import SubunitTestRunner
class TestSubunitTestRunner(TestCase):
def test_includes_timing_output(self):
io = BytesIO()
runner = SubunitTestRunner(stream=io)
test = PlaceHolder('name')
runner.run(test)
io.seek(0)
eventstream = StreamResult()
subunit.ByteStreamToStreamResult(io).run(eventstream)
timestamps = [event[-1] for event in eventstream._events
if event is not None]
self.assertNotEqual([], timestamps)
def test_enumerates_tests_before_run(self):
io = BytesIO()
runner = SubunitTestRunner(stream=io)
test1 = PlaceHolder('name1')
test2 = PlaceHolder('name2')
case = unittest.TestSuite([test1, test2])
runner.run(case)
io.seek(0)
eventstream = StreamResult()
subunit.ByteStreamToStreamResult(io).run(eventstream)
self.assertEqual([
('status', 'name1', 'exists'),
('status', 'name2', 'exists'),
], [event[:3] for event in eventstream._events[:2]])
def test_list_errors_if_errors_from_list_test(self):
io = BytesIO()
runner = SubunitTestRunner(stream=io)
def list_test(test):
return [], ['failed import']
self.patch(run, 'list_test', list_test)
exc = self.assertRaises(SystemExit, runner.list, None)
self.assertEqual((2,), exc.args)

View File

@@ -0,0 +1,346 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 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.
#
"""Tests for subunit.TestResultFilter."""
from datetime import datetime
import os
import subprocess
import sys
from subunit import iso8601
import unittest
from testtools import TestCase
from testtools.compat import _b, BytesIO
from testtools.testresult.doubles import ExtendedTestResult, StreamResult
import subunit
from subunit.test_results import make_tag_filter, TestResultFilter
from subunit import ByteStreamToStreamResult, StreamResultToBytes
class TestTestResultFilter(TestCase):
"""Test for TestResultFilter, a TestResult object which filters tests."""
# While TestResultFilter works on python objects, using a subunit stream
# is an easy pithy way of getting a series of test objects to call into
# the TestResult, and as TestResultFilter is intended for use with subunit
# also has the benefit of detecting any interface skew issues.
example_subunit_stream = _b("""\
tags: global
test passed
success passed
test failed
tags: local
failure failed
test error
error error [
error details
]
test skipped
skip skipped
test todo
xfail todo
""")
def run_tests(self, result_filter, input_stream=None):
"""Run tests through the given filter.
:param result_filter: A filtering TestResult object.
:param input_stream: Bytes of subunit stream data. If not provided,
uses TestTestResultFilter.example_subunit_stream.
"""
if input_stream is None:
input_stream = self.example_subunit_stream
test = subunit.ProtocolTestCase(BytesIO(input_stream))
test.run(result_filter)
def test_default(self):
"""The default is to exclude success and include everything else."""
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result)
self.run_tests(result_filter)
# skips are seen as success by default python TestResult.
self.assertEqual(['error'],
[error[0].id() for error in filtered_result.errors])
self.assertEqual(['failed'],
[failure[0].id() for failure in
filtered_result.failures])
self.assertEqual(4, filtered_result.testsRun)
def test_tag_filter(self):
tag_filter = make_tag_filter(['global'], ['local'])
result = ExtendedTestResult()
result_filter = TestResultFilter(
result, filter_success=False, filter_predicate=tag_filter)
self.run_tests(result_filter)
tests_included = [
event[1] for event in result._events if event[0] == 'startTest']
tests_expected = list(map(
subunit.RemotedTestCase,
['passed', 'error', 'skipped', 'todo']))
self.assertEquals(tests_expected, tests_included)
def test_tags_tracked_correctly(self):
tag_filter = make_tag_filter(['a'], [])
result = ExtendedTestResult()
result_filter = TestResultFilter(
result, filter_success=False, filter_predicate=tag_filter)
input_stream = _b(
"test: foo\n"
"tags: a\n"
"successful: foo\n"
"test: bar\n"
"successful: bar\n")
self.run_tests(result_filter, input_stream)
foo = subunit.RemotedTestCase('foo')
self.assertEquals(
[('startTest', foo),
('tags', set(['a']), set()),
('addSuccess', foo),
('stopTest', foo),
],
result._events)
def test_exclude_errors(self):
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result, filter_error=True)
self.run_tests(result_filter)
# skips are seen as errors by default python TestResult.
self.assertEqual([], filtered_result.errors)
self.assertEqual(['failed'],
[failure[0].id() for failure in
filtered_result.failures])
self.assertEqual(3, filtered_result.testsRun)
def test_fixup_expected_failures(self):
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result,
fixup_expected_failures=set(["failed"]))
self.run_tests(result_filter)
self.assertEqual(['failed', 'todo'],
[failure[0].id() for failure in filtered_result.expectedFailures])
self.assertEqual([], filtered_result.failures)
self.assertEqual(4, filtered_result.testsRun)
def test_fixup_expected_errors(self):
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result,
fixup_expected_failures=set(["error"]))
self.run_tests(result_filter)
self.assertEqual(['error', 'todo'],
[failure[0].id() for failure in filtered_result.expectedFailures])
self.assertEqual([], filtered_result.errors)
self.assertEqual(4, filtered_result.testsRun)
def test_fixup_unexpected_success(self):
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result, filter_success=False,
fixup_expected_failures=set(["passed"]))
self.run_tests(result_filter)
self.assertEqual(['passed'],
[passed.id() for passed in filtered_result.unexpectedSuccesses])
self.assertEqual(5, filtered_result.testsRun)
def test_exclude_failure(self):
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result, filter_failure=True)
self.run_tests(result_filter)
self.assertEqual(['error'],
[error[0].id() for error in filtered_result.errors])
self.assertEqual([],
[failure[0].id() for failure in
filtered_result.failures])
self.assertEqual(3, filtered_result.testsRun)
def test_exclude_skips(self):
filtered_result = subunit.TestResultStats(None)
result_filter = TestResultFilter(filtered_result, filter_skip=True)
self.run_tests(result_filter)
self.assertEqual(0, filtered_result.skipped_tests)
self.assertEqual(2, filtered_result.failed_tests)
self.assertEqual(3, filtered_result.testsRun)
def test_include_success(self):
"""Successes can be included if requested."""
filtered_result = unittest.TestResult()
result_filter = TestResultFilter(filtered_result,
filter_success=False)
self.run_tests(result_filter)
self.assertEqual(['error'],
[error[0].id() for error in filtered_result.errors])
self.assertEqual(['failed'],
[failure[0].id() for failure in
filtered_result.failures])
self.assertEqual(5, filtered_result.testsRun)
def test_filter_predicate(self):
"""You can filter by predicate callbacks"""
# 0.0.7 and earlier did not support the 'tags' parameter, so we need
# to test that we still support behaviour without it.
filtered_result = unittest.TestResult()
def filter_cb(test, outcome, err, details):
return outcome == 'success'
result_filter = TestResultFilter(filtered_result,
filter_predicate=filter_cb,
filter_success=False)
self.run_tests(result_filter)
# Only success should pass
self.assertEqual(1, filtered_result.testsRun)
def test_filter_predicate_with_tags(self):
"""You can filter by predicate callbacks that accept tags"""
filtered_result = unittest.TestResult()
def filter_cb(test, outcome, err, details, tags):
return outcome == 'success'
result_filter = TestResultFilter(filtered_result,
filter_predicate=filter_cb,
filter_success=False)
self.run_tests(result_filter)
# Only success should pass
self.assertEqual(1, filtered_result.testsRun)
def test_time_ordering_preserved(self):
# Passing a subunit stream through TestResultFilter preserves the
# relative ordering of 'time' directives and any other subunit
# directives that are still included.
date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC)
date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC)
date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC)
subunit_stream = _b('\n'.join([
"time: %s",
"test: foo",
"time: %s",
"error: foo",
"time: %s",
""]) % (date_a, date_b, date_c))
result = ExtendedTestResult()
result_filter = TestResultFilter(result)
self.run_tests(result_filter, subunit_stream)
foo = subunit.RemotedTestCase('foo')
self.maxDiff = None
self.assertEqual(
[('time', date_a),
('time', date_b),
('startTest', foo),
('addError', foo, {}),
('stopTest', foo),
('time', date_c)], result._events)
def test_time_passes_through_filtered_tests(self):
# Passing a subunit stream through TestResultFilter preserves 'time'
# directives even if a specific test is filtered out.
date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC)
date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC)
date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC)
subunit_stream = _b('\n'.join([
"time: %s",
"test: foo",
"time: %s",
"success: foo",
"time: %s",
""]) % (date_a, date_b, date_c))
result = ExtendedTestResult()
result_filter = TestResultFilter(result)
result_filter.startTestRun()
self.run_tests(result_filter, subunit_stream)
result_filter.stopTestRun()
foo = subunit.RemotedTestCase('foo')
self.maxDiff = None
self.assertEqual(
[('startTestRun',),
('time', date_a),
('time', date_c),
('stopTestRun',),], result._events)
def test_skip_preserved(self):
subunit_stream = _b('\n'.join([
"test: foo",
"skip: foo",
""]))
result = ExtendedTestResult()
result_filter = TestResultFilter(result)
self.run_tests(result_filter, subunit_stream)
foo = subunit.RemotedTestCase('foo')
self.assertEquals(
[('startTest', foo),
('addSkip', foo, {}),
('stopTest', foo), ], result._events)
if sys.version_info < (2, 7):
# These tests require Python >=2.7.
del test_fixup_expected_failures, test_fixup_expected_errors, test_fixup_unexpected_success
class TestFilterCommand(TestCase):
def run_command(self, args, stream):
root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
script_path = os.path.join(root, 'filters', 'subunit-filter')
command = [sys.executable, script_path] + list(args)
ps = subprocess.Popen(
command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = ps.communicate(stream)
if ps.returncode != 0:
raise RuntimeError("%s failed: %s" % (command, err))
return out
def test_default(self):
byte_stream = BytesIO()
stream = StreamResultToBytes(byte_stream)
stream.status(test_id="foo", test_status="inprogress")
stream.status(test_id="foo", test_status="skip")
output = self.run_command([], byte_stream.getvalue())
events = StreamResult()
ByteStreamToStreamResult(BytesIO(output)).run(events)
ids = set(event[1] for event in events._events)
self.assertEqual([
('status', 'foo', 'inprogress'),
('status', 'foo', 'skip'),
], [event[:3] for event in events._events])
def test_tags(self):
byte_stream = BytesIO()
stream = StreamResultToBytes(byte_stream)
stream.status(
test_id="foo", test_status="inprogress", test_tags=set(["a"]))
stream.status(
test_id="foo", test_status="success", test_tags=set(["a"]))
stream.status(test_id="bar", test_status="inprogress")
stream.status(test_id="bar", test_status="inprogress")
stream.status(
test_id="baz", test_status="inprogress", test_tags=set(["a"]))
stream.status(
test_id="baz", test_status="success", test_tags=set(["a"]))
output = self.run_command(
['-s', '--with-tag', 'a'], byte_stream.getvalue())
events = StreamResult()
ByteStreamToStreamResult(BytesIO(output)).run(events)
ids = set(event[1] for event in events._events)
self.assertEqual(set(['foo', 'baz']), ids)
def test_no_passthrough(self):
output = self.run_command(['--no-passthrough'], b'hi thar')
self.assertEqual(b'', output)
def test_passthrough(self):
output = self.run_command([], b'hi thar')
byte_stream = BytesIO()
stream = StreamResultToBytes(byte_stream)
stream.status(file_name="stdout", file_bytes=b'hi thar')
self.assertEqual(byte_stream.getvalue(), output)

View File

@@ -0,0 +1,78 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 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.
#
"""Tests for subunit.TestResultStats."""
import unittest
from testtools.compat import _b, BytesIO, StringIO
import subunit
class TestTestResultStats(unittest.TestCase):
"""Test for TestResultStats, a TestResult object that generates stats."""
def setUp(self):
self.output = StringIO()
self.result = subunit.TestResultStats(self.output)
self.input_stream = BytesIO()
self.test = subunit.ProtocolTestCase(self.input_stream)
def test_stats_empty(self):
self.test.run(self.result)
self.assertEqual(0, self.result.total_tests)
self.assertEqual(0, self.result.passed_tests)
self.assertEqual(0, self.result.failed_tests)
self.assertEqual(set(), self.result.seen_tags)
def setUpUsedStream(self):
self.input_stream.write(_b("""tags: global
test passed
success passed
test failed
tags: local
failure failed
test error
error error
test skipped
skip skipped
test todo
xfail todo
"""))
self.input_stream.seek(0)
self.test.run(self.result)
def test_stats_smoke_everything(self):
# Statistics are calculated usefully.
self.setUpUsedStream()
self.assertEqual(5, self.result.total_tests)
self.assertEqual(2, self.result.passed_tests)
self.assertEqual(2, self.result.failed_tests)
self.assertEqual(1, self.result.skipped_tests)
self.assertEqual(set(["global", "local"]), self.result.seen_tags)
def test_stat_formatting(self):
expected = ("""
Total tests: 5
Passed tests: 2
Failed tests: 2
Skipped tests: 1
Seen tags: global, local
""")[1:]
self.setUpUsedStream()
self.result.formatStats()
self.assertEqual(expected, self.output.getvalue())

View File

@@ -0,0 +1,85 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 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.
#
"""Tests for subunit.tag_stream."""
from io import BytesIO
import testtools
from testtools.matchers import Contains
import subunit
import subunit.test_results
class TestSubUnitTags(testtools.TestCase):
def setUp(self):
super(TestSubUnitTags, self).setUp()
self.original = BytesIO()
self.filtered = BytesIO()
def test_add_tag(self):
# Literal values to avoid set sort-order dependencies. Python code show
# derivation.
# reference = BytesIO()
# stream = subunit.StreamResultToBytes(reference)
# stream.status(
# test_id='test', test_status='inprogress', test_tags=set(['quux', 'foo']))
# stream.status(
# test_id='test', test_status='success', test_tags=set(['bar', 'quux', 'foo']))
reference = [
b'\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86\xb3)'
b'\x83\x1b\x04test\x03\x03bar\x04quux\x03fooqn\xab)',
b'\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86\xb3)'
b'\x83\x1b\x04test\x03\x04quux\x03foo\x03bar\xaf\xbd\x9d\xd6',
b'\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86\xb3)'
b'\x83\x1b\x04test\x03\x04quux\x03bar\x03foo\x03\x04b\r',
b'\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86\xb3)'
b'\x83\x1b\x04test\x03\x03bar\x03foo\x04quux\xd2\x18\x1bC',
b'\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec\xb3)'
b'\x83\x1b\x04test\x03\x03foo\x04quux\x03bar\x08\xc2X\x83',
b'\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec\xb3)'
b'\x83\x1b\x04test\x03\x03bar\x03foo\x04quux\xd2\x18\x1bC',
b'\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec\xb3)'
b'\x83\x1b\x04test\x03\x03foo\x03bar\x04quux:\x05e\x80',
]
stream = subunit.StreamResultToBytes(self.original)
stream.status(
test_id='test', test_status='inprogress', test_tags=set(['foo']))
stream.status(
test_id='test', test_status='success', test_tags=set(['foo', 'bar']))
self.original.seek(0)
self.assertEqual(
0, subunit.tag_stream(self.original, self.filtered, ["quux"]))
self.assertThat(reference, Contains(self.filtered.getvalue()))
def test_remove_tag(self):
reference = BytesIO()
stream = subunit.StreamResultToBytes(reference)
stream.status(
test_id='test', test_status='inprogress', test_tags=set(['foo']))
stream.status(
test_id='test', test_status='success', test_tags=set(['foo']))
stream = subunit.StreamResultToBytes(self.original)
stream.status(
test_id='test', test_status='inprogress', test_tags=set(['foo']))
stream.status(
test_id='test', test_status='success', test_tags=set(['foo', 'bar']))
self.original.seek(0)
self.assertEqual(
0, subunit.tag_stream(self.original, self.filtered, ["-bar"]))
self.assertEqual(reference.getvalue(), self.filtered.getvalue())

View File

@@ -0,0 +1,387 @@
#
# subunit: extensions to python unittest to get test results from subprocesses.
# Copyright (C) 2005 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.
#
"""Tests for TAP2SubUnit."""
from io import BytesIO, StringIO
import unittest
from testtools import TestCase
from testtools.compat import _u
from testtools.testresult.doubles import StreamResult
import subunit
UTF8_TEXT = 'text/plain; charset=UTF8'
class TestTAP2SubUnit(TestCase):
"""Tests for TAP2SubUnit.
These tests test TAP string data in, and subunit string data out.
This is ok because the subunit protocol is intended to be stable,
but it might be easier/pithier to write tests against TAP string in,
parsed subunit objects out (by hooking the subunit stream to a subunit
protocol server.
"""
def setUp(self):
super(TestTAP2SubUnit, self).setUp()
self.tap = StringIO()
self.subunit = BytesIO()
def test_skip_entire_file(self):
# A file
# 1..- # Skipped: comment
# results in a single skipped test.
self.tap.write(_u("1..0 # Skipped: entire file skipped\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'file skip', 'skip', None, True,
'tap comment', b'Skipped: entire file skipped', True, None, None,
None)])
def test_ok_test_pass(self):
# A file
# ok
# results in a passed test with name 'test 1' (a synthetic name as tap
# does not require named fixtures - it is the first test in the tap
# stream).
self.tap.write(_u("ok\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'success', None, False, None,
None, True, None, None, None)])
def test_ok_test_number_pass(self):
# A file
# ok 1
# results in a passed test with name 'test 1'
self.tap.write(_u("ok 1\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'success', None, False, None,
None, True, None, None, None)])
def test_ok_test_number_description_pass(self):
# A file
# ok 1 - There is a description
# results in a passed test with name 'test 1 - There is a description'
self.tap.write(_u("ok 1 - There is a description\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1 - There is a description',
'success', None, False, None, None, True, None, None, None)])
def test_ok_test_description_pass(self):
# A file
# ok There is a description
# results in a passed test with name 'test 1 There is a description'
self.tap.write(_u("ok There is a description\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1 There is a description',
'success', None, False, None, None, True, None, None, None)])
def test_ok_SKIP_skip(self):
# A file
# ok # SKIP
# results in a skkip test with name 'test 1'
self.tap.write(_u("ok # SKIP\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'skip', None, False, None,
None, True, None, None, None)])
def test_ok_skip_number_comment_lowercase(self):
self.tap.write(_u("ok 1 # skip no samba environment available, skipping compilation\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'skip', None, False, 'tap comment',
b'no samba environment available, skipping compilation', True,
'text/plain; charset=UTF8', None, None)])
def test_ok_number_description_SKIP_skip_comment(self):
# A file
# ok 1 foo # SKIP Not done yet
# results in a skip test with name 'test 1 foo' and a log of
# Not done yet
self.tap.write(_u("ok 1 foo # SKIP Not done yet\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1 foo', 'skip', None, False,
'tap comment', b'Not done yet', True, 'text/plain; charset=UTF8',
None, None)])
def test_ok_SKIP_skip_comment(self):
# A file
# ok # SKIP Not done yet
# results in a skip test with name 'test 1' and a log of Not done yet
self.tap.write(_u("ok # SKIP Not done yet\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'skip', None, False,
'tap comment', b'Not done yet', True, 'text/plain; charset=UTF8',
None, None)])
def test_ok_TODO_xfail(self):
# A file
# ok # TODO
# results in a xfail test with name 'test 1'
self.tap.write(_u("ok # TODO\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'xfail', None, False, None,
None, True, None, None, None)])
def test_ok_TODO_xfail_comment(self):
# A file
# ok # TODO Not done yet
# results in a xfail test with name 'test 1' and a log of Not done yet
self.tap.write(_u("ok # TODO Not done yet\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([('status', 'test 1', 'xfail', None, False,
'tap comment', b'Not done yet', True, 'text/plain; charset=UTF8',
None, None)])
def test_bail_out_errors(self):
# A file with line in it
# Bail out! COMMENT
# is treated as an error
self.tap.write(_u("ok 1 foo\n"))
self.tap.write(_u("Bail out! Lifejacket engaged\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 foo', 'success', None, False, None, None, True,
None, None, None),
('status', 'Bail out! Lifejacket engaged', 'fail', None, False,
None, None, True, None, None, None)])
def test_missing_test_at_end_with_plan_adds_error(self):
# A file
# 1..3
# ok first test
# not ok third test
# results in three tests, with the third being created
self.tap.write(_u('1..3\n'))
self.tap.write(_u('ok first test\n'))
self.tap.write(_u('not ok second test\n'))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 first test', 'success', None, False, None,
None, True, None, None, None),
('status', 'test 2 second test', 'fail', None, False, None, None,
True, None, None, None),
('status', 'test 3', 'fail', None, False, 'tap meta',
b'test missing from TAP output', True, 'text/plain; charset=UTF8',
None, None)])
def test_missing_test_with_plan_adds_error(self):
# A file
# 1..3
# ok first test
# not ok 3 third test
# results in three tests, with the second being created
self.tap.write(_u('1..3\n'))
self.tap.write(_u('ok first test\n'))
self.tap.write(_u('not ok 3 third test\n'))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 first test', 'success', None, False, None, None,
True, None, None, None),
('status', 'test 2', 'fail', None, False, 'tap meta',
b'test missing from TAP output', True, 'text/plain; charset=UTF8',
None, None),
('status', 'test 3 third test', 'fail', None, False, None, None,
True, None, None, None)])
def test_missing_test_no_plan_adds_error(self):
# A file
# ok first test
# not ok 3 third test
# results in three tests, with the second being created
self.tap.write(_u('ok first test\n'))
self.tap.write(_u('not ok 3 third test\n'))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 first test', 'success', None, False, None, None,
True, None, None, None),
('status', 'test 2', 'fail', None, False, 'tap meta',
b'test missing from TAP output', True, 'text/plain; charset=UTF8',
None, None),
('status', 'test 3 third test', 'fail', None, False, None, None,
True, None, None, None)])
def test_four_tests_in_a_row_trailing_plan(self):
# A file
# ok 1 - first test in a script with no plan at all
# not ok 2 - second
# ok 3 - third
# not ok 4 - fourth
# 1..4
# results in four tests numbered and named
self.tap.write(_u('ok 1 - first test in a script with trailing plan\n'))
self.tap.write(_u('not ok 2 - second\n'))
self.tap.write(_u('ok 3 - third\n'))
self.tap.write(_u('not ok 4 - fourth\n'))
self.tap.write(_u('1..4\n'))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 - first test in a script with trailing plan',
'success', None, False, None, None, True, None, None, None),
('status', 'test 2 - second', 'fail', None, False, None, None,
True, None, None, None),
('status', 'test 3 - third', 'success', None, False, None, None,
True, None, None, None),
('status', 'test 4 - fourth', 'fail', None, False, None, None,
True, None, None, None)])
def test_four_tests_in_a_row_with_plan(self):
# A file
# 1..4
# ok 1 - first test in a script with no plan at all
# not ok 2 - second
# ok 3 - third
# not ok 4 - fourth
# results in four tests numbered and named
self.tap.write(_u('1..4\n'))
self.tap.write(_u('ok 1 - first test in a script with a plan\n'))
self.tap.write(_u('not ok 2 - second\n'))
self.tap.write(_u('ok 3 - third\n'))
self.tap.write(_u('not ok 4 - fourth\n'))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 - first test in a script with a plan',
'success', None, False, None, None, True, None, None, None),
('status', 'test 2 - second', 'fail', None, False, None, None,
True, None, None, None),
('status', 'test 3 - third', 'success', None, False, None, None,
True, None, None, None),
('status', 'test 4 - fourth', 'fail', None, False, None, None,
True, None, None, None)])
def test_four_tests_in_a_row_no_plan(self):
# A file
# ok 1 - first test in a script with no plan at all
# not ok 2 - second
# ok 3 - third
# not ok 4 - fourth
# results in four tests numbered and named
self.tap.write(_u('ok 1 - first test in a script with no plan at all\n'))
self.tap.write(_u('not ok 2 - second\n'))
self.tap.write(_u('ok 3 - third\n'))
self.tap.write(_u('not ok 4 - fourth\n'))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1 - first test in a script with no plan at all',
'success', None, False, None, None, True, None, None, None),
('status', 'test 2 - second', 'fail', None, False, None, None,
True, None, None, None),
('status', 'test 3 - third', 'success', None, False, None, None,
True, None, None, None),
('status', 'test 4 - fourth', 'fail', None, False, None, None,
True, None, None, None)])
def test_todo_and_skip(self):
# A file
# not ok 1 - a fail but # TODO but is TODO
# not ok 2 - another fail # SKIP instead
# results in two tests, numbered and commented.
self.tap.write(_u("not ok 1 - a fail but # TODO but is TODO\n"))
self.tap.write(_u("not ok 2 - another fail # SKIP instead\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.subunit.seek(0)
events = StreamResult()
subunit.ByteStreamToStreamResult(self.subunit).run(events)
self.check_events([
('status', 'test 1 - a fail but', 'xfail', None, False,
'tap comment', b'but is TODO', True, 'text/plain; charset=UTF8',
None, None),
('status', 'test 2 - another fail', 'skip', None, False,
'tap comment', b'instead', True, 'text/plain; charset=UTF8',
None, None)])
def test_leading_comments_add_to_next_test_log(self):
# A file
# # comment
# ok
# ok
# results in a single test with the comment included
# in the first test and not the second.
self.tap.write(_u("# comment\n"))
self.tap.write(_u("ok\n"))
self.tap.write(_u("ok\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1', 'success', None, False, 'tap comment',
b'# comment', True, 'text/plain; charset=UTF8', None, None),
('status', 'test 2', 'success', None, False, None, None, True,
None, None, None)])
def test_trailing_comments_are_included_in_last_test_log(self):
# A file
# ok foo
# ok foo
# # comment
# results in a two tests, with the second having the comment
# attached to its log.
self.tap.write(_u("ok\n"))
self.tap.write(_u("ok\n"))
self.tap.write(_u("# comment\n"))
self.tap.seek(0)
result = subunit.TAP2SubUnit(self.tap, self.subunit)
self.assertEqual(0, result)
self.check_events([
('status', 'test 1', 'success', None, False, None, None, True,
None, None, None),
('status', 'test 2', 'success', None, False, 'tap comment',
b'# comment', True, 'text/plain; charset=UTF8', None, None)])
def check_events(self, events):
self.subunit.seek(0)
eventstream = StreamResult()
subunit.ByteStreamToStreamResult(self.subunit).run(eventstream)
self.assertEqual(events, eventstream._events)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,436 @@
#
# 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.
#
from io import BytesIO
import datetime
from testtools import TestCase
from testtools.matchers import Contains, HasLength
from testtools.tests.test_testresult import TestStreamResultContract
from testtools.testresult.doubles import StreamResult
import subunit
import subunit.iso8601 as iso8601
CONSTANT_ENUM = b'\xb3)\x01\x0c\x03foo\x08U_\x1b'
CONSTANT_INPROGRESS = b'\xb3)\x02\x0c\x03foo\x8e\xc1-\xb5'
CONSTANT_SUCCESS = b'\xb3)\x03\x0c\x03fooE\x9d\xfe\x10'
CONSTANT_UXSUCCESS = b'\xb3)\x04\x0c\x03fooX\x98\xce\xa8'
CONSTANT_SKIP = b'\xb3)\x05\x0c\x03foo\x93\xc4\x1d\r'
CONSTANT_FAIL = b'\xb3)\x06\x0c\x03foo\x15Po\xa3'
CONSTANT_XFAIL = b'\xb3)\x07\x0c\x03foo\xde\x0c\xbc\x06'
CONSTANT_EOF = b'\xb3!\x10\x08S\x15\x88\xdc'
CONSTANT_FILE_CONTENT = b'\xb3!@\x13\x06barney\x03wooA5\xe3\x8c'
CONSTANT_MIME = b'\xb3! #\x1aapplication/foo; charset=1x3Q\x15'
CONSTANT_TIMESTAMP = b'\xb3+\x03\x13<\x17T\xcf\x80\xaf\xc8\x03barI\x96>-'
CONSTANT_ROUTE_CODE = b'\xb3-\x03\x13\x03bar\x06source\x9cY9\x19'
CONSTANT_RUNNABLE = b'\xb3(\x03\x0c\x03foo\xe3\xea\xf5\xa4'
CONSTANT_TAGS = [
b'\xb3)\x80\x15\x03bar\x02\x03foo\x03barTHn\xb4',
b'\xb3)\x80\x15\x03bar\x02\x03bar\x03foo\xf8\xf1\x91o',
]
class TestStreamResultToBytesContract(TestCase, TestStreamResultContract):
"""Check that StreamResult behaves as testtools expects."""
def _make_result(self):
return subunit.StreamResultToBytes(BytesIO())
class TestStreamResultToBytes(TestCase):
def _make_result(self):
output = BytesIO()
return subunit.StreamResultToBytes(output), output
def test_numbers(self):
result = subunit.StreamResultToBytes(BytesIO())
packet = []
self.assertRaises(Exception, result._write_number, -1, packet)
self.assertEqual([], packet)
result._write_number(0, packet)
self.assertEqual([b'\x00'], packet)
del packet[:]
result._write_number(63, packet)
self.assertEqual([b'\x3f'], packet)
del packet[:]
result._write_number(64, packet)
self.assertEqual([b'\x40\x40'], packet)
del packet[:]
result._write_number(16383, packet)
self.assertEqual([b'\x7f\xff'], packet)
del packet[:]
result._write_number(16384, packet)
self.assertEqual([b'\x80\x40', b'\x00'], packet)
del packet[:]
result._write_number(4194303, packet)
self.assertEqual([b'\xbf\xff', b'\xff'], packet)
del packet[:]
result._write_number(4194304, packet)
self.assertEqual([b'\xc0\x40\x00\x00'], packet)
del packet[:]
result._write_number(1073741823, packet)
self.assertEqual([b'\xff\xff\xff\xff'], packet)
del packet[:]
self.assertRaises(Exception, result._write_number, 1073741824, packet)
self.assertEqual([], packet)
def test_volatile_length(self):
# if the length of the packet data before the length itself is
# considered is right on the boundary for length's variable length
# encoding, it is easy to get the length wrong by not accounting for
# length itself.
# that is, the encoder has to ensure that length == sum (length_of_rest
# + length_of_length)
result, output = self._make_result()
# 1 byte short:
result.status(file_name="", file_bytes=b'\xff'*0)
self.assertThat(output.getvalue(), HasLength(10))
self.assertEqual(b'\x0a', output.getvalue()[3:4])
output.seek(0)
output.truncate()
# 1 byte long:
result.status(file_name="", file_bytes=b'\xff'*53)
self.assertThat(output.getvalue(), HasLength(63))
self.assertEqual(b'\x3f', output.getvalue()[3:4])
output.seek(0)
output.truncate()
# 2 bytes short
result.status(file_name="", file_bytes=b'\xff'*54)
self.assertThat(output.getvalue(), HasLength(65))
self.assertEqual(b'\x40\x41', output.getvalue()[3:5])
output.seek(0)
output.truncate()
# 2 bytes long
result.status(file_name="", file_bytes=b'\xff'*16371)
self.assertThat(output.getvalue(), HasLength(16383))
self.assertEqual(b'\x7f\xff', output.getvalue()[3:5])
output.seek(0)
output.truncate()
# 3 bytes short
result.status(file_name="", file_bytes=b'\xff'*16372)
self.assertThat(output.getvalue(), HasLength(16385))
self.assertEqual(b'\x80\x40\x01', output.getvalue()[3:6])
output.seek(0)
output.truncate()
# 3 bytes long
result.status(file_name="", file_bytes=b'\xff'*4194289)
self.assertThat(output.getvalue(), HasLength(4194303))
self.assertEqual(b'\xbf\xff\xff', output.getvalue()[3:6])
output.seek(0)
output.truncate()
self.assertRaises(Exception, result.status, file_name="",
file_bytes=b'\xff'*4194290)
def test_trivial_enumeration(self):
result, output = self._make_result()
result.status("foo", 'exists')
self.assertEqual(CONSTANT_ENUM, output.getvalue())
def test_inprogress(self):
result, output = self._make_result()
result.status("foo", 'inprogress')
self.assertEqual(CONSTANT_INPROGRESS, output.getvalue())
def test_success(self):
result, output = self._make_result()
result.status("foo", 'success')
self.assertEqual(CONSTANT_SUCCESS, output.getvalue())
def test_uxsuccess(self):
result, output = self._make_result()
result.status("foo", 'uxsuccess')
self.assertEqual(CONSTANT_UXSUCCESS, output.getvalue())
def test_skip(self):
result, output = self._make_result()
result.status("foo", 'skip')
self.assertEqual(CONSTANT_SKIP, output.getvalue())
def test_fail(self):
result, output = self._make_result()
result.status("foo", 'fail')
self.assertEqual(CONSTANT_FAIL, output.getvalue())
def test_xfail(self):
result, output = self._make_result()
result.status("foo", 'xfail')
self.assertEqual(CONSTANT_XFAIL, output.getvalue())
def test_unknown_status(self):
result, output = self._make_result()
self.assertRaises(Exception, result.status, "foo", 'boo')
self.assertEqual(b'', output.getvalue())
def test_eof(self):
result, output = self._make_result()
result.status(eof=True)
self.assertEqual(CONSTANT_EOF, output.getvalue())
def test_file_content(self):
result, output = self._make_result()
result.status(file_name="barney", file_bytes=b"woo")
self.assertEqual(CONSTANT_FILE_CONTENT, output.getvalue())
def test_mime(self):
result, output = self._make_result()
result.status(mime_type="application/foo; charset=1")
self.assertEqual(CONSTANT_MIME, output.getvalue())
def test_route_code(self):
result, output = self._make_result()
result.status(test_id="bar", test_status='success',
route_code="source")
self.assertEqual(CONSTANT_ROUTE_CODE, output.getvalue())
def test_runnable(self):
result, output = self._make_result()
result.status("foo", 'success', runnable=False)
self.assertEqual(CONSTANT_RUNNABLE, output.getvalue())
def test_tags(self):
result, output = self._make_result()
result.status(test_id="bar", test_tags=set(['foo', 'bar']))
self.assertThat(CONSTANT_TAGS, Contains(output.getvalue()))
def test_timestamp(self):
timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45,
iso8601.Utc())
result, output = self._make_result()
result.status(test_id="bar", test_status='success', timestamp=timestamp)
self.assertEqual(CONSTANT_TIMESTAMP, output.getvalue())
class TestByteStreamToStreamResult(TestCase):
def test_non_subunit_encapsulated(self):
source = BytesIO(b"foo\nbar\n")
result = StreamResult()
subunit.ByteStreamToStreamResult(
source, non_subunit_name="stdout").run(result)
self.assertEqual([
('status', None, None, None, True, 'stdout', b'f', False, None, None, None),
('status', None, None, None, True, 'stdout', b'o', False, None, None, None),
('status', None, None, None, True, 'stdout', b'o', False, None, None, None),
('status', None, None, None, True, 'stdout', b'\n', False, None, None, None),
('status', None, None, None, True, 'stdout', b'b', False, None, None, None),
('status', None, None, None, True, 'stdout', b'a', False, None, None, None),
('status', None, None, None, True, 'stdout', b'r', False, None, None, None),
('status', None, None, None, True, 'stdout', b'\n', False, None, None, None),
], result._events)
self.assertEqual(b'', source.read())
def test_signature_middle_utf8_char(self):
utf8_bytes = b'\xe3\xb3\x8a'
source = BytesIO(utf8_bytes)
# Should be treated as one character (it is u'\u3cca') and wrapped
result = StreamResult()
subunit.ByteStreamToStreamResult(
source, non_subunit_name="stdout").run(
result)
self.assertEqual([
('status', None, None, None, True, 'stdout', b'\xe3', False, None, None, None),
('status', None, None, None, True, 'stdout', b'\xb3', False, None, None, None),
('status', None, None, None, True, 'stdout', b'\x8a', False, None, None, None),
], result._events)
def test_non_subunit_disabled_raises(self):
source = BytesIO(b"foo\nbar\n")
result = StreamResult()
case = subunit.ByteStreamToStreamResult(source)
e = self.assertRaises(Exception, case.run, result)
self.assertEqual(b'f', e.args[1])
self.assertEqual(b'oo\nbar\n', source.read())
self.assertEqual([], result._events)
def test_trivial_enumeration(self):
source = BytesIO(CONSTANT_ENUM)
result = StreamResult()
subunit.ByteStreamToStreamResult(
source, non_subunit_name="stdout").run(result)
self.assertEqual(b'', source.read())
self.assertEqual([
('status', 'foo', 'exists', None, True, None, None, False, None, None, None),
], result._events)
def test_multiple_events(self):
source = BytesIO(CONSTANT_ENUM + CONSTANT_ENUM)
result = StreamResult()
subunit.ByteStreamToStreamResult(
source, non_subunit_name="stdout").run(result)
self.assertEqual(b'', source.read())
self.assertEqual([
('status', 'foo', 'exists', None, True, None, None, False, None, None, None),
('status', 'foo', 'exists', None, True, None, None, False, None, None, None),
], result._events)
def test_inprogress(self):
self.check_event(CONSTANT_INPROGRESS, 'inprogress')
def test_success(self):
self.check_event(CONSTANT_SUCCESS, 'success')
def test_uxsuccess(self):
self.check_event(CONSTANT_UXSUCCESS, 'uxsuccess')
def test_skip(self):
self.check_event(CONSTANT_SKIP, 'skip')
def test_fail(self):
self.check_event(CONSTANT_FAIL, 'fail')
def test_xfail(self):
self.check_event(CONSTANT_XFAIL, 'xfail')
def check_events(self, source_bytes, events):
source = BytesIO(source_bytes)
result = StreamResult()
subunit.ByteStreamToStreamResult(
source, non_subunit_name="stdout").run(result)
self.assertEqual(b'', source.read())
self.assertEqual(events, result._events)
#- any file attachments should be byte contents [as users assume that].
for event in result._events:
if event[5] is not None:
self.assertIsInstance(event[6], bytes)
def check_event(self, source_bytes, test_status=None, test_id="foo",
route_code=None, timestamp=None, tags=None, mime_type=None,
file_name=None, file_bytes=None, eof=False, runnable=True):
event = self._event(test_id=test_id, test_status=test_status,
tags=tags, runnable=runnable, file_name=file_name,
file_bytes=file_bytes, eof=eof, mime_type=mime_type,
route_code=route_code, timestamp=timestamp)
self.check_events(source_bytes, [event])
def _event(self, test_status=None, test_id=None, route_code=None,
timestamp=None, tags=None, mime_type=None, file_name=None,
file_bytes=None, eof=False, runnable=True):
return ('status', test_id, test_status, tags, runnable, file_name,
file_bytes, eof, mime_type, route_code, timestamp)
def test_eof(self):
self.check_event(CONSTANT_EOF, test_id=None, eof=True)
def test_file_content(self):
self.check_event(CONSTANT_FILE_CONTENT,
test_id=None, file_name="barney", file_bytes=b"woo")
def test_file_content_length_into_checksum(self):
# A bad file content length which creeps into the checksum.
bad_file_length_content = b'\xb3!@\x13\x06barney\x04woo\xdc\xe2\xdb\x35'
self.check_events(bad_file_length_content, [
self._event(test_id="subunit.parser", eof=True,
file_name="Packet data", file_bytes=bad_file_length_content,
mime_type="application/octet-stream"),
self._event(test_id="subunit.parser", test_status="fail", eof=True,
file_name="Parser Error",
file_bytes=b"File content extends past end of packet: claimed 4 bytes, 3 available",
mime_type="text/plain;charset=utf8"),
])
def test_packet_length_4_word_varint(self):
packet_data = b'\xb3!@\xc0\x00\x11'
self.check_events(packet_data, [
self._event(test_id="subunit.parser", eof=True,
file_name="Packet data", file_bytes=packet_data,
mime_type="application/octet-stream"),
self._event(test_id="subunit.parser", test_status="fail", eof=True,
file_name="Parser Error",
file_bytes=b"3 byte maximum given but 4 byte value found.",
mime_type="text/plain;charset=utf8"),
])
def test_mime(self):
self.check_event(CONSTANT_MIME,
test_id=None, mime_type='application/foo; charset=1')
def test_route_code(self):
self.check_event(CONSTANT_ROUTE_CODE,
'success', route_code="source", test_id="bar")
def test_runnable(self):
self.check_event(CONSTANT_RUNNABLE,
test_status='success', runnable=False)
def test_tags(self):
self.check_event(CONSTANT_TAGS[0],
None, tags=set(['foo', 'bar']), test_id="bar")
def test_timestamp(self):
timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45,
iso8601.Utc())
self.check_event(CONSTANT_TIMESTAMP,
'success', test_id='bar', timestamp=timestamp)
def test_bad_crc_errors_via_status(self):
file_bytes = CONSTANT_MIME[:-1] + b'\x00'
self.check_events( file_bytes, [
self._event(test_id="subunit.parser", eof=True,
file_name="Packet data", file_bytes=file_bytes,
mime_type="application/octet-stream"),
self._event(test_id="subunit.parser", test_status="fail", eof=True,
file_name="Parser Error",
file_bytes=b'Bad checksum - calculated (0x78335115), '
b'stored (0x78335100)',
mime_type="text/plain;charset=utf8"),
])
def test_not_utf8_in_string(self):
file_bytes = CONSTANT_ROUTE_CODE[:5] + b'\xb4' + CONSTANT_ROUTE_CODE[6:-4] + b'\xce\x56\xc6\x17'
self.check_events(file_bytes, [
self._event(test_id="subunit.parser", eof=True,
file_name="Packet data", file_bytes=file_bytes,
mime_type="application/octet-stream"),
self._event(test_id="subunit.parser", test_status="fail", eof=True,
file_name="Parser Error",
file_bytes=b'UTF8 string at offset 2 is not UTF8',
mime_type="text/plain;charset=utf8"),
])
def test_NULL_in_string(self):
file_bytes = CONSTANT_ROUTE_CODE[:6] + b'\x00' + CONSTANT_ROUTE_CODE[7:-4] + b'\xd7\x41\xac\xfe'
self.check_events(file_bytes, [
self._event(test_id="subunit.parser", eof=True,
file_name="Packet data", file_bytes=file_bytes,
mime_type="application/octet-stream"),
self._event(test_id="subunit.parser", test_status="fail", eof=True,
file_name="Parser Error",
file_bytes=b'UTF8 string at offset 2 contains NUL byte',
mime_type="text/plain;charset=utf8"),
])
def test_bad_utf8_stringlength(self):
file_bytes = CONSTANT_ROUTE_CODE[:4] + b'\x3f' + CONSTANT_ROUTE_CODE[5:-4] + b'\xbe\x29\xe0\xc2'
self.check_events(file_bytes, [
self._event(test_id="subunit.parser", eof=True,
file_name="Packet data", file_bytes=file_bytes,
mime_type="application/octet-stream"),
self._event(test_id="subunit.parser", test_status="fail", eof=True,
file_name="Parser Error",
file_bytes=b'UTF8 string at offset 2 extends past end of '
b'packet: claimed 63 bytes, 10 available',
mime_type="text/plain;charset=utf8"),
])
def test_route_code_and_file_content(self):
content = BytesIO()
subunit.StreamResultToBytes(content).status(
route_code='0', mime_type='text/plain', file_name='bar',
file_bytes=b'foo')
self.check_event(content.getvalue(), test_id=None, file_name='bar',
route_code='0', mime_type='text/plain', file_bytes=b'foo')

View File

@@ -0,0 +1,566 @@
#
# subunit: extensions to Python unittest to get test results from subprocesses.
# Copyright (C) 2009 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 csv
import datetime
import sys
import unittest
from testtools import TestCase
from testtools.compat import StringIO
from testtools.content import (
text_content,
TracebackContent,
)
from testtools.testresult.doubles import ExtendedTestResult
import subunit
import subunit.iso8601 as iso8601
import subunit.test_results
import testtools
class LoggingDecorator(subunit.test_results.HookedTestResultDecorator):
def __init__(self, decorated):
self._calls = 0
super(LoggingDecorator, self).__init__(decorated)
def _before_event(self):
self._calls += 1
class AssertBeforeTestResult(LoggingDecorator):
"""A TestResult for checking preconditions."""
def __init__(self, decorated, test):
self.test = test
super(AssertBeforeTestResult, self).__init__(decorated)
def _before_event(self):
self.test.assertEqual(1, self.earlier._calls)
super(AssertBeforeTestResult, self)._before_event()
class TimeCapturingResult(unittest.TestResult):
def __init__(self):
super(TimeCapturingResult, self).__init__()
self._calls = []
self.failfast = False
def time(self, a_datetime):
self._calls.append(a_datetime)
class TestHookedTestResultDecorator(unittest.TestCase):
def setUp(self):
# An end to the chain
terminal = unittest.TestResult()
# Asserts that the call was made to self.result before asserter was
# called.
asserter = AssertBeforeTestResult(terminal, self)
# The result object we call, which much increase its call count.
self.result = LoggingDecorator(asserter)
asserter.earlier = self.result
self.decorated = asserter
def tearDown(self):
# The hook in self.result must have been called
self.assertEqual(1, self.result._calls)
# The hook in asserter must have been called too, otherwise the
# assertion about ordering won't have completed.
self.assertEqual(1, self.decorated._calls)
def test_startTest(self):
self.result.startTest(self)
def test_startTestRun(self):
self.result.startTestRun()
def test_stopTest(self):
self.result.stopTest(self)
def test_stopTestRun(self):
self.result.stopTestRun()
def test_addError(self):
self.result.addError(self, subunit.RemoteError())
def test_addError_details(self):
self.result.addError(self, details={})
def test_addFailure(self):
self.result.addFailure(self, subunit.RemoteError())
def test_addFailure_details(self):
self.result.addFailure(self, details={})
def test_addSuccess(self):
self.result.addSuccess(self)
def test_addSuccess_details(self):
self.result.addSuccess(self, details={})
def test_addSkip(self):
self.result.addSkip(self, "foo")
def test_addSkip_details(self):
self.result.addSkip(self, details={})
def test_addExpectedFailure(self):
self.result.addExpectedFailure(self, subunit.RemoteError())
def test_addExpectedFailure_details(self):
self.result.addExpectedFailure(self, details={})
def test_addUnexpectedSuccess(self):
self.result.addUnexpectedSuccess(self)
def test_addUnexpectedSuccess_details(self):
self.result.addUnexpectedSuccess(self, details={})
def test_progress(self):
self.result.progress(1, subunit.PROGRESS_SET)
def test_wasSuccessful(self):
self.result.wasSuccessful()
def test_shouldStop(self):
self.result.shouldStop
def test_stop(self):
self.result.stop()
def test_time(self):
self.result.time(None)
class TestAutoTimingTestResultDecorator(unittest.TestCase):
def setUp(self):
# And end to the chain which captures time events.
terminal = TimeCapturingResult()
# The result object under test.
self.result = subunit.test_results.AutoTimingTestResultDecorator(
terminal)
self.decorated = terminal
def test_without_time_calls_time_is_called_and_not_None(self):
self.result.startTest(self)
self.assertEqual(1, len(self.decorated._calls))
self.assertNotEqual(None, self.decorated._calls[0])
def test_no_time_from_progress(self):
self.result.progress(1, subunit.PROGRESS_CUR)
self.assertEqual(0, len(self.decorated._calls))
def test_no_time_from_shouldStop(self):
self.decorated.stop()
self.result.shouldStop
self.assertEqual(0, len(self.decorated._calls))
def test_calling_time_inhibits_automatic_time(self):
# Calling time() outputs a time signal immediately and prevents
# automatically adding one when other methods are called.
time = datetime.datetime(2009,10,11,12,13,14,15, iso8601.Utc())
self.result.time(time)
self.result.startTest(self)
self.result.stopTest(self)
self.assertEqual(1, len(self.decorated._calls))
self.assertEqual(time, self.decorated._calls[0])
def test_calling_time_None_enables_automatic_time(self):
time = datetime.datetime(2009,10,11,12,13,14,15, iso8601.Utc())
self.result.time(time)
self.assertEqual(1, len(self.decorated._calls))
self.assertEqual(time, self.decorated._calls[0])
# Calling None passes the None through, in case other results care.
self.result.time(None)
self.assertEqual(2, len(self.decorated._calls))
self.assertEqual(None, self.decorated._calls[1])
# Calling other methods doesn't generate an automatic time event.
self.result.startTest(self)
self.assertEqual(3, len(self.decorated._calls))
self.assertNotEqual(None, self.decorated._calls[2])
def test_set_failfast_True(self):
self.assertFalse(self.decorated.failfast)
self.result.failfast = True
self.assertTrue(self.decorated.failfast)
class TestTagCollapsingDecorator(TestCase):
def test_tags_collapsed_outside_of_tests(self):
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
tag_collapser.tags(set(['a']), set())
tag_collapser.tags(set(['b']), set())
tag_collapser.startTest(self)
self.assertEquals(
[('tags', set(['a', 'b']), set([])),
('startTest', self),
], result._events)
def test_tags_collapsed_outside_of_tests_are_flushed(self):
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
tag_collapser.startTestRun()
tag_collapser.tags(set(['a']), set())
tag_collapser.tags(set(['b']), set())
tag_collapser.startTest(self)
tag_collapser.addSuccess(self)
tag_collapser.stopTest(self)
tag_collapser.stopTestRun()
self.assertEquals(
[('startTestRun',),
('tags', set(['a', 'b']), set([])),
('startTest', self),
('addSuccess', self),
('stopTest', self),
('stopTestRun',),
], result._events)
def test_tags_forwarded_after_tests(self):
test = subunit.RemotedTestCase('foo')
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
tag_collapser.startTestRun()
tag_collapser.startTest(test)
tag_collapser.addSuccess(test)
tag_collapser.stopTest(test)
tag_collapser.tags(set(['a']), set(['b']))
tag_collapser.stopTestRun()
self.assertEqual(
[('startTestRun',),
('startTest', test),
('addSuccess', test),
('stopTest', test),
('tags', set(['a']), set(['b'])),
('stopTestRun',),
],
result._events)
def test_tags_collapsed_inside_of_tests(self):
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
test = subunit.RemotedTestCase('foo')
tag_collapser.startTest(test)
tag_collapser.tags(set(['a']), set())
tag_collapser.tags(set(['b']), set(['a']))
tag_collapser.tags(set(['c']), set())
tag_collapser.stopTest(test)
self.assertEquals(
[('startTest', test),
('tags', set(['b', 'c']), set(['a'])),
('stopTest', test)],
result._events)
def test_tags_collapsed_inside_of_tests_different_ordering(self):
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
test = subunit.RemotedTestCase('foo')
tag_collapser.startTest(test)
tag_collapser.tags(set(), set(['a']))
tag_collapser.tags(set(['a', 'b']), set())
tag_collapser.tags(set(['c']), set())
tag_collapser.stopTest(test)
self.assertEquals(
[('startTest', test),
('tags', set(['a', 'b', 'c']), set()),
('stopTest', test)],
result._events)
def test_tags_sent_before_result(self):
# Because addSuccess and friends tend to send subunit output
# immediately, and because 'tags:' before a result line means
# something different to 'tags:' after a result line, we need to be
# sure that tags are emitted before 'addSuccess' (or whatever).
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
test = subunit.RemotedTestCase('foo')
tag_collapser.startTest(test)
tag_collapser.tags(set(['a']), set())
tag_collapser.addSuccess(test)
tag_collapser.stopTest(test)
self.assertEquals(
[('startTest', test),
('tags', set(['a']), set()),
('addSuccess', test),
('stopTest', test)],
result._events)
class TestTimeCollapsingDecorator(TestCase):
def make_time(self):
# Heh heh.
return datetime.datetime(
2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC)
def test_initial_time_forwarded(self):
# We always forward the first time event we see.
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
a_time = self.make_time()
tag_collapser.time(a_time)
self.assertEquals([('time', a_time)], result._events)
def test_time_collapsed_to_first_and_last(self):
# If there are many consecutive time events, only the first and last
# are sent through.
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
times = [self.make_time() for i in range(5)]
for a_time in times:
tag_collapser.time(a_time)
tag_collapser.startTest(subunit.RemotedTestCase('foo'))
self.assertEquals(
[('time', times[0]), ('time', times[-1])], result._events[:-1])
def test_only_one_time_sent(self):
# If we receive a single time event followed by a non-time event, we
# send exactly one time event.
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
a_time = self.make_time()
tag_collapser.time(a_time)
tag_collapser.startTest(subunit.RemotedTestCase('foo'))
self.assertEquals([('time', a_time)], result._events[:-1])
def test_duplicate_times_not_sent(self):
# Many time events with the exact same time are collapsed into one
# time event.
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
a_time = self.make_time()
for i in range(5):
tag_collapser.time(a_time)
tag_collapser.startTest(subunit.RemotedTestCase('foo'))
self.assertEquals([('time', a_time)], result._events[:-1])
def test_no_times_inserted(self):
result = ExtendedTestResult()
tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
a_time = self.make_time()
tag_collapser.time(a_time)
foo = subunit.RemotedTestCase('foo')
tag_collapser.startTest(foo)
tag_collapser.addSuccess(foo)
tag_collapser.stopTest(foo)
self.assertEquals(
[('time', a_time),
('startTest', foo),
('addSuccess', foo),
('stopTest', foo)], result._events)
class TestByTestResultTests(testtools.TestCase):
def setUp(self):
super(TestByTestResultTests, self).setUp()
self.log = []
self.result = subunit.test_results.TestByTestResult(self.on_test)
if sys.version_info >= (3, 0):
self.result._now = iter(range(5)).__next__
else:
self.result._now = iter(range(5)).next
def assertCalled(self, **kwargs):
defaults = {
'test': self,
'tags': set(),
'details': None,
'start_time': 0,
'stop_time': 1,
}
defaults.update(kwargs)
self.assertEqual([defaults], self.log)
def on_test(self, **kwargs):
self.log.append(kwargs)
def test_no_tests_nothing_reported(self):
self.result.startTestRun()
self.result.stopTestRun()
self.assertEqual([], self.log)
def test_add_success(self):
self.result.startTest(self)
self.result.addSuccess(self)
self.result.stopTest(self)
self.assertCalled(status='success')
def test_add_success_details(self):
self.result.startTest(self)
details = {'foo': 'bar'}
self.result.addSuccess(self, details=details)
self.result.stopTest(self)
self.assertCalled(status='success', details=details)
def test_tags(self):
if not getattr(self.result, 'tags', None):
self.skipTest("No tags in testtools")
self.result.tags(['foo'], [])
self.result.startTest(self)
self.result.addSuccess(self)
self.result.stopTest(self)
self.assertCalled(status='success', tags=set(['foo']))
def test_add_error(self):
self.result.startTest(self)
try:
1/0
except ZeroDivisionError:
error = sys.exc_info()
self.result.addError(self, error)
self.result.stopTest(self)
self.assertCalled(
status='error',
details={'traceback': TracebackContent(error, self)})
def test_add_error_details(self):
self.result.startTest(self)
details = {"foo": text_content("bar")}
self.result.addError(self, details=details)
self.result.stopTest(self)
self.assertCalled(status='error', details=details)
def test_add_failure(self):
self.result.startTest(self)
try:
self.fail("intentional failure")
except self.failureException:
failure = sys.exc_info()
self.result.addFailure(self, failure)
self.result.stopTest(self)
self.assertCalled(
status='failure',
details={'traceback': TracebackContent(failure, self)})
def test_add_failure_details(self):
self.result.startTest(self)
details = {"foo": text_content("bar")}
self.result.addFailure(self, details=details)
self.result.stopTest(self)
self.assertCalled(status='failure', details=details)
def test_add_xfail(self):
self.result.startTest(self)
try:
1/0
except ZeroDivisionError:
error = sys.exc_info()
self.result.addExpectedFailure(self, error)
self.result.stopTest(self)
self.assertCalled(
status='xfail',
details={'traceback': TracebackContent(error, self)})
def test_add_xfail_details(self):
self.result.startTest(self)
details = {"foo": text_content("bar")}
self.result.addExpectedFailure(self, details=details)
self.result.stopTest(self)
self.assertCalled(status='xfail', details=details)
def test_add_unexpected_success(self):
self.result.startTest(self)
details = {'foo': 'bar'}
self.result.addUnexpectedSuccess(self, details=details)
self.result.stopTest(self)
self.assertCalled(status='success', details=details)
def test_add_skip_reason(self):
self.result.startTest(self)
reason = self.getUniqueString()
self.result.addSkip(self, reason)
self.result.stopTest(self)
self.assertCalled(
status='skip', details={'reason': text_content(reason)})
def test_add_skip_details(self):
self.result.startTest(self)
details = {'foo': 'bar'}
self.result.addSkip(self, details=details)
self.result.stopTest(self)
self.assertCalled(status='skip', details=details)
def test_twice(self):
self.result.startTest(self)
self.result.addSuccess(self, details={'foo': 'bar'})
self.result.stopTest(self)
self.result.startTest(self)
self.result.addSuccess(self)
self.result.stopTest(self)
self.assertEqual(
[{'test': self,
'status': 'success',
'start_time': 0,
'stop_time': 1,
'tags': set(),
'details': {'foo': 'bar'}},
{'test': self,
'status': 'success',
'start_time': 2,
'stop_time': 3,
'tags': set(),
'details': None},
],
self.log)
class TestCsvResult(testtools.TestCase):
def parse_stream(self, stream):
stream.seek(0)
reader = csv.reader(stream)
return list(reader)
def test_csv_output(self):
stream = StringIO()
result = subunit.test_results.CsvResult(stream)
if sys.version_info >= (3, 0):
result._now = iter(range(5)).__next__
else:
result._now = iter(range(5)).next
result.startTestRun()
result.startTest(self)
result.addSuccess(self)
result.stopTest(self)
result.stopTestRun()
self.assertEqual(
[['test', 'status', 'start_time', 'stop_time'],
[self.id(), 'success', '0', '1'],
],
self.parse_stream(stream))
def test_just_header_when_no_tests(self):
stream = StringIO()
result = subunit.test_results.CsvResult(stream)
result.startTestRun()
result.stopTestRun()
self.assertEqual(
[['test', 'status', 'start_time', 'stop_time']],
self.parse_stream(stream))
def test_no_output_before_events(self):
stream = StringIO()
subunit.test_results.CsvResult(stream)
self.assertEqual([], self.parse_stream(stream))

View File

@@ -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,))