1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
|
import ast
import functools
import inspect
import operator
import re
import shlex
import textwrap
from plover import system
from plover.formatting import Formatter
from plover.steno import normalize_steno
from plover.steno_dictionary import StenoDictionary
from plover.translation import Translator
from .output import CaptureOutput
from .steno import steno_to_stroke
BLACKBOX_OUTPUT_RX = re.compile("r?['\"]")
def blackbox_setup(blackbox):
blackbox.output = CaptureOutput()
blackbox.formatter = Formatter()
blackbox.formatter.set_output(blackbox.output)
blackbox.translator = Translator()
blackbox.translator.set_min_undo_length(100)
blackbox.translator.add_listener(blackbox.formatter.format)
blackbox.dictionary = blackbox.translator.get_dictionary()
blackbox.dictionary.set_dicts([StenoDictionary()])
def blackbox_replay(blackbox, name, test):
# Hide from traceback on assertions (reduce output size for failed tests).
__tracebackhide__ = operator.methodcaller("errisinstance", AssertionError)
definitions, instructions = test.strip().rsplit("\n\n", 1)
for entry in definitions.split("\n"):
if entry.startswith(":"):
_blackbox_replay_action(blackbox, entry[1:])
continue
for steno, translation in ast.literal_eval("{" + entry + "}").items():
blackbox.dictionary.set(normalize_steno(steno), translation)
# Track line number for a more user-friendly assertion message.
lines = test.split("\n")
lnum = len(lines) - 3 - test.rstrip().rsplit("\n\n", 1)[1].count("\n")
for step in re.split("(?<=[^\\\\])\n", instructions):
# Mark current instruction's line.
lnum += 1
step = step.strip()
# Support for changing some settings on the fly.
if step.startswith(":"):
_blackbox_replay_action(blackbox, step[1:])
continue
steno, output = step.split(None, 1)
steno = list(map(steno_to_stroke, normalize_steno(steno.strip())))
output = output.strip()
assert_msg = (
name
+ "\n"
+ "\n".join(("> " if n == lnum else " ") + l for n, l in enumerate(lines))
+ "\n"
)
if BLACKBOX_OUTPUT_RX.match(output):
# Replay strokes.
list(map(blackbox.translator.translate, steno))
# Check output.
expected_output = ast.literal_eval(output)
assert_msg += (
" " + repr(blackbox.output.text) + "\n!= " + repr(expected_output)
)
assert blackbox.output.text == expected_output, assert_msg
elif output.startswith("raise "):
expected_exception = output[6:].strip()
try:
list(map(blackbox.translator.translate, steno))
except Exception as e:
exception_class = e.__class__.__name__
else:
exception_class = "None"
assert_msg += " " + exception_class + "\n!= " + expected_exception
assert exception_class == expected_exception, assert_msg
else:
raise ValueError("invalid output:\n%s" % output)
def _blackbox_replay_action(blackbox, action_spec):
action, *args = shlex.split(action_spec)
if action == "start_attached":
assert not args
blackbox.formatter.start_attached = True
elif action == "spaces_after":
assert not args
blackbox.formatter.set_space_placement("After Output")
elif action == "spaces_before":
assert not args
blackbox.formatter.set_space_placement("Before Output")
elif action == "system":
assert len(args) == 1
system.setup(args[0])
else:
raise ValueError("invalid action:\n%r" % action_spec)
def blackbox_test(cls_or_fn):
# If a class is decorated, we wrap all its test_ methods.
if inspect.isclass(cls_or_fn):
# Create a subclass to avoid modifying the original class
class WrapperClass(cls_or_fn):
pass
# Iterate over all attributes in the class.
for name in dir(WrapperClass):
# Only wrap methods whose name starts with "test_"
if name.startswith("test_"):
fn = getattr(WrapperClass, name)
# Recursively apply the decorator to each test function.
new_fn = blackbox_test(fn)
setattr(WrapperClass, name, new_fn)
# Return the wrappped class
return WrapperClass
else:
# For a single test function:
name = cls_or_fn.__name__
# Dedent the function's docstring which contains the blackbox test script.
test = textwrap.dedent(cls_or_fn.__doc__)
@functools.wraps(cls_or_fn)
def wrapper(bb, *args, **kwargs):
# Set up the blackbox environment:
# - This initializes the output capture, formatter, translator,
# and dictionary used for replaying strokes.
blackbox_setup(bb)
# Execute the original test function.
cls_or_fn(bb, *args, **kwargs)
# Replay the test instructions captured in the dedented docstring.
# This verifies that the outputs match what is expected.
blackbox_replay(bb, name, test)
# Return the wrapper function that runs setup, the test, and replay.
return wrapper
|