208 lines
7.3 KiB
Python
208 lines
7.3 KiB
Python
# subunit: extensions to python unittest to get test results from subprocesses.
|
|
# Copyright (C) 2013 Subunit Contributors
|
|
#
|
|
# 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 datetime
|
|
from functools import partial
|
|
from optparse import (
|
|
OptionGroup,
|
|
OptionParser,
|
|
OptionValueError,
|
|
)
|
|
import sys
|
|
|
|
from subunit import make_stream_binary
|
|
from subunit.iso8601 import UTC
|
|
from subunit.v2 import StreamResultToBytes
|
|
|
|
|
|
_FINAL_ACTIONS = frozenset([
|
|
'exists',
|
|
'fail',
|
|
'skip',
|
|
'success',
|
|
'uxsuccess',
|
|
'xfail',
|
|
])
|
|
_ALL_ACTIONS = _FINAL_ACTIONS.union(['inprogress'])
|
|
_CHUNK_SIZE=3670016 # 3.5 MiB
|
|
|
|
|
|
def output_main():
|
|
args = parse_arguments()
|
|
output = StreamResultToBytes(sys.stdout)
|
|
generate_stream_results(args, output)
|
|
return 0
|
|
|
|
|
|
def parse_arguments(args=None, ParserClass=OptionParser):
|
|
"""Parse arguments from the command line.
|
|
|
|
If specified, args must be a list of strings, similar to sys.argv[1:].
|
|
|
|
ParserClass may be specified to override the class we use to parse the
|
|
command-line arguments. This is useful for testing.
|
|
"""
|
|
parser = ParserClass(
|
|
prog="subunit-output",
|
|
description="A tool to generate a subunit v2 result byte-stream",
|
|
usage="subunit-output [-h] [status TEST_ID] [options]",
|
|
)
|
|
parser.set_default('tags', None)
|
|
parser.set_default('test_id', None)
|
|
|
|
status_commands = OptionGroup(
|
|
parser,
|
|
"Status Commands",
|
|
"These options report the status of a test. TEST_ID must be a string "
|
|
"that uniquely identifies the test."
|
|
)
|
|
for action_name in _ALL_ACTIONS:
|
|
status_commands.add_option(
|
|
"--%s" % action_name,
|
|
nargs=1,
|
|
action="callback",
|
|
callback=set_status_cb,
|
|
callback_args=(action_name,),
|
|
dest="action",
|
|
metavar="TEST_ID",
|
|
help="Report a test status."
|
|
)
|
|
parser.add_option_group(status_commands)
|
|
|
|
file_commands = OptionGroup(
|
|
parser,
|
|
"File Options",
|
|
"These options control attaching data to a result stream. They can "
|
|
"either be specified with a status command, in which case the file "
|
|
"is attached to the test status, or by themselves, in which case "
|
|
"the file is attached to the stream (and not associated with any "
|
|
"test id)."
|
|
)
|
|
file_commands.add_option(
|
|
"--attach-file",
|
|
help="Attach a file to the result stream for this test. If '-' is "
|
|
"specified, stdin will be read instead. In this case, the file "
|
|
"name will be set to 'stdin' (but can still be overridden with "
|
|
"the --file-name option)."
|
|
)
|
|
file_commands.add_option(
|
|
"--file-name",
|
|
help="The name to give this file attachment. If not specified, the "
|
|
"name of the file on disk will be used, or 'stdin' in the case "
|
|
"where '-' was passed to the '--attach-file' argument. This option"
|
|
" may only be specified when '--attach-file' is specified.",
|
|
)
|
|
file_commands.add_option(
|
|
"--mimetype",
|
|
help="The mime type to send with this file. This is only used if the "
|
|
"--attach-file argument is used. This argument is optional. If it "
|
|
"is not specified, the file will be sent without a mime type. This "
|
|
"option may only be specified when '--attach-file' is specified.",
|
|
default=None
|
|
)
|
|
parser.add_option_group(file_commands)
|
|
|
|
parser.add_option(
|
|
"--tag",
|
|
help="Specifies a tag. May be used multiple times",
|
|
action="append",
|
|
dest="tags",
|
|
default=[]
|
|
)
|
|
|
|
(options, args) = parser.parse_args(args)
|
|
if options.mimetype and not options.attach_file:
|
|
parser.error("Cannot specify --mimetype without --attach-file")
|
|
if options.file_name and not options.attach_file:
|
|
parser.error("Cannot specify --file-name without --attach-file")
|
|
if options.attach_file:
|
|
if options.attach_file == '-':
|
|
if not options.file_name:
|
|
options.file_name = 'stdin'
|
|
options.attach_file = make_stream_binary(sys.stdin)
|
|
else:
|
|
try:
|
|
options.attach_file = open(options.attach_file, 'rb')
|
|
except IOError as e:
|
|
parser.error("Cannot open %s (%s)" % (options.attach_file, e.strerror))
|
|
if options.tags and not options.action:
|
|
parser.error("Cannot specify --tag without a status command")
|
|
if not (options.attach_file or options.action):
|
|
parser.error("Must specify either --attach-file or a status command")
|
|
|
|
return options
|
|
|
|
|
|
def set_status_cb(option, opt_str, value, parser, status_name):
|
|
if getattr(parser.values, "action", None) is not None:
|
|
raise OptionValueError("argument %s: Only one status may be specified at once." % opt_str)
|
|
|
|
if len(parser.rargs) == 0:
|
|
raise OptionValueError("argument %s: must specify a single TEST_ID." % opt_str)
|
|
parser.values.action = status_name
|
|
parser.values.test_id = parser.rargs.pop(0)
|
|
|
|
|
|
def generate_stream_results(args, output_writer):
|
|
output_writer.startTestRun()
|
|
|
|
if args.attach_file:
|
|
reader = partial(args.attach_file.read, _CHUNK_SIZE)
|
|
this_file_hunk = reader()
|
|
next_file_hunk = reader()
|
|
|
|
is_first_packet = True
|
|
is_last_packet = False
|
|
while not is_last_packet:
|
|
write_status = output_writer.status
|
|
|
|
if is_first_packet:
|
|
if args.attach_file:
|
|
if args.mimetype:
|
|
write_status = partial(write_status, mime_type=args.mimetype)
|
|
if args.tags:
|
|
write_status = partial(write_status, test_tags=args.tags)
|
|
write_status = partial(write_status, timestamp=create_timestamp())
|
|
if args.action not in _FINAL_ACTIONS:
|
|
write_status = partial(write_status, test_status=args.action)
|
|
is_first_packet = False
|
|
|
|
if args.attach_file:
|
|
filename = args.file_name or args.attach_file.name
|
|
write_status = partial(write_status, file_name=filename, file_bytes=this_file_hunk)
|
|
if next_file_hunk == b'':
|
|
write_status = partial(write_status, eof=True)
|
|
is_last_packet = True
|
|
else:
|
|
this_file_hunk = next_file_hunk
|
|
next_file_hunk = reader()
|
|
else:
|
|
is_last_packet = True
|
|
|
|
if args.test_id:
|
|
write_status = partial(write_status, test_id=args.test_id)
|
|
|
|
if is_last_packet:
|
|
if args.action in _FINAL_ACTIONS:
|
|
write_status = partial(write_status, test_status=args.action)
|
|
|
|
write_status()
|
|
|
|
output_writer.stopTestRun()
|
|
|
|
|
|
def create_timestamp():
|
|
return datetime.datetime.now(UTC)
|