Compare commits
88 Commits
fix
...
testcase-ex
| Author | SHA1 | Date | |
|---|---|---|---|
| 4402611aaa | |||
| abb7557070 | |||
| bbd8d7c5c8 | |||
| 6fb6425cf4 | |||
| acfc5772ee | |||
| fd55d9f010 | |||
| 5e494d18b3 | |||
| 266c69e71d | |||
| 2c431f7a08 | |||
| 51070c6d0e | |||
| a2232535e8 | |||
| 459295f97e | |||
| 641f5698b7 | |||
| eb3f6a2e66 | |||
| 19fa815048 | |||
| 8342a66d9b | |||
| 7aa9d66597 | |||
| aa9f15a199 | |||
| c6cd0fe411 | |||
| 11cc6aebf8 | |||
| ef98d82679 | |||
| e5d16a984b | |||
| 3eba03854b | |||
| 692bf4c34b | |||
| 451c3a7baf | |||
| 3dec7f6498 | |||
| 29f4c1caee | |||
| 3543238e5b | |||
| 70ce1aa5c7 | |||
| 56ee681250 | |||
| 0f8b1ade00 | |||
| ca77d6f8d7 | |||
| 14e19cd9d0 | |||
| 29979fd828 | |||
| 777ff166a8 | |||
| 57abd5bd0f | |||
| 6c08d61125 | |||
| 12b070c7c7 | |||
| f9995e88fd | |||
| a6f2e6b2ce | |||
| 144001b412 | |||
| 6b585d2227 | |||
| 322d247bcd | |||
| 12cc2d45c9 | |||
| 13e8445144 | |||
| 5c80ffa509 | |||
| 4adaddafae | |||
| 4517f76396 | |||
| a85c53c436 | |||
| 3344defb89 | |||
| 1e24279acc | |||
| 42b38f32c6 | |||
| 73339577cf | |||
| d49c392383 | |||
| 9d8155ba17 | |||
| 4a3b27285b | |||
| e8888933ea | |||
| b671e01f19 | |||
| 84b73d7164 | |||
| 1398dc08d0 | |||
| 98ee23888d | |||
| 70cb354f81 | |||
| 479d19bb9b | |||
| 15744d14c1 | |||
| a9f5024399 | |||
| 671685ea64 | |||
| e2f2baca94 | |||
| 92556b3a66 | |||
| 1c26110256 | |||
| 75383c00c2 | |||
| 44d3155a6f | |||
| 64e35b7d49 | |||
| 3f23fb10a3 | |||
| 2a54a20a37 | |||
| eb048b5e03 | |||
| fadf4b0e1c | |||
| b627cf66a2 | |||
| e40561a5ae | |||
| ac9c91eeec | |||
| 1efa5b6178 | |||
| 83d48b2428 | |||
| b1b613a247 | |||
| 98eba57192 | |||
| 36210285ea | |||
| 2b2f4a5d1d | |||
| 148cd16ae6 | |||
| eba7bf474d | |||
| 595a94c44c |
@@ -196,6 +196,7 @@ screen front_page_project:
|
||||
|
||||
textbutton _("Navigate Script") action Jump("navigation")
|
||||
textbutton _("Check Script (Lint)") action Jump("lint")
|
||||
textbutton _("Launch tests") action Jump("launch_tests")
|
||||
|
||||
if project.current.exists("game/gui.rpy"):
|
||||
textbutton _("Change/Update GUI") action Jump("change_gui")
|
||||
@@ -283,3 +284,11 @@ label force_recompile:
|
||||
project.current.launch([ 'compile' ], wait=True)
|
||||
|
||||
jump front_page
|
||||
|
||||
label launch_tests:
|
||||
|
||||
python hide:
|
||||
interface.processing(_("Launching tests..."))
|
||||
project.current.launch([ 'test' ], wait=True)
|
||||
|
||||
jump front_page
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
init python:
|
||||
TEST_PROJECTS = u"/tmp/renpy-moé"
|
||||
import shutil
|
||||
def finally_callback():
|
||||
print("finally "*100)
|
||||
whether = renpy.is_in_test()
|
||||
|
||||
define config.end_testcase_callbacks += [finally_callback]
|
||||
|
||||
testcase default:
|
||||
python hide:
|
||||
renpy.watch("renpy.is_in_test()")
|
||||
renpy.watch("waitch")
|
||||
renpy.watch("whether")
|
||||
waitch = 5
|
||||
"preferences"
|
||||
"theme"
|
||||
"dEfAuLt theme"
|
||||
"theme"
|
||||
"dark the"
|
||||
"return"
|
||||
# exit
|
||||
"quit"
|
||||
|
||||
|
||||
init python:
|
||||
TEST_PROJECTS = "/tmp/renpy-moé"
|
||||
import shutil
|
||||
|
||||
testcase old_default:
|
||||
|
||||
call new_project
|
||||
call translate_project
|
||||
|
||||
+2
-2
@@ -665,7 +665,7 @@ class Node(object):
|
||||
be executed after this one.
|
||||
"""
|
||||
|
||||
raise Exception("Node subclass forgot to define execute.")
|
||||
raise NotImplementedError("Node subclass forgot to define execute.")
|
||||
|
||||
# def early_execute(self):
|
||||
# """
|
||||
@@ -719,7 +719,7 @@ class Node(object):
|
||||
text in the statement.
|
||||
"""
|
||||
|
||||
raise Exception("Not Implemented")
|
||||
raise NotImplementedError
|
||||
|
||||
def analyze(self):
|
||||
"""
|
||||
|
||||
@@ -308,6 +308,10 @@ You may be using a system install of python. Please run {0}.sh,
|
||||
|
||||
finally:
|
||||
|
||||
if renpy.test.testexecution.node is not None:
|
||||
for clbk in renpy.config.end_testcase_callbacks:
|
||||
clbk()
|
||||
|
||||
if "RENPY_SHUTDOWN_TRACE" in os.environ:
|
||||
enable_trace(int(os.environ["RENPY_SHUTDOWN_TRACE"]))
|
||||
|
||||
|
||||
@@ -1377,6 +1377,9 @@ early_developer = False
|
||||
# A function that returns the autosave prefix.
|
||||
autosave_prefix_callback = None
|
||||
|
||||
# Functions called when a test ends - one way or another
|
||||
end_testcase_callbacks = [ ]
|
||||
|
||||
# Compatibility for c492f2f8e6de57b2740fa5d00566f445124caaba.
|
||||
at_transform_compare_full_context = False
|
||||
|
||||
|
||||
@@ -4302,6 +4302,17 @@ def add_to_all_stores(name, value):
|
||||
ns[name] = value
|
||||
|
||||
|
||||
def is_in_test():
|
||||
"""
|
||||
:doc: other
|
||||
|
||||
Returns True if Ren'py is currently executing a testcase.
|
||||
This always returns False at init time.
|
||||
"""
|
||||
|
||||
return (renpy.test.testexecution.node is not None)
|
||||
|
||||
|
||||
def get_zorder_list(layer):
|
||||
"""
|
||||
:doc: image_func
|
||||
@@ -4436,6 +4447,19 @@ def set_focus(screen, id, layer="screens"): # @ReservedAssignment
|
||||
restart_interaction()
|
||||
|
||||
|
||||
def has_default_focus():
|
||||
"""
|
||||
:doc: other
|
||||
|
||||
Returns true if the game can be advanced by clicking or pressing the
|
||||
spacebar.
|
||||
|
||||
For example, it is True in a dialogue or a pause, and False in places
|
||||
like the Game Menu.
|
||||
"""
|
||||
return any(f.x is None for f in renpy.display.focus.focus_list)
|
||||
|
||||
|
||||
def check_permission(permission):
|
||||
"""
|
||||
:doc: android_permission
|
||||
|
||||
+193
-77
@@ -42,6 +42,9 @@ class TestSettings(renpy.object.Object):
|
||||
# How long should we wait for a transition before we proceed?
|
||||
self.transition_timeout = 5.0
|
||||
|
||||
# How many times should we try to find a good spot to place the mouse?
|
||||
self.focus_trials = 100
|
||||
|
||||
_test = TestSettings()
|
||||
|
||||
|
||||
@@ -49,7 +52,7 @@ class Node(object):
|
||||
"""
|
||||
An AST node for a test script.
|
||||
"""
|
||||
|
||||
__slots__ = ("filename", "linenumber")
|
||||
def __init__(self, loc):
|
||||
self.filename, self.linenumber = loc
|
||||
|
||||
@@ -60,6 +63,7 @@ class Node(object):
|
||||
This is expected to return a state, or None to advance to the next
|
||||
node.
|
||||
"""
|
||||
return True
|
||||
|
||||
def execute(self, state, t):
|
||||
"""
|
||||
@@ -71,14 +75,12 @@ class Node(object):
|
||||
`t`
|
||||
The time since start was called.
|
||||
"""
|
||||
|
||||
return state
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Returns True if this node is ready to execute, or False otherwise.
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def report(self):
|
||||
@@ -86,21 +88,26 @@ class Node(object):
|
||||
Reports the location of this statement. This should only be called
|
||||
in the execute method of leaf nodes of the test tree.
|
||||
"""
|
||||
|
||||
renpy.test.testexecution.node_loc = (self.filename, self.linenumber)
|
||||
|
||||
|
||||
class Pattern(Node):
|
||||
class Clause(Node):
|
||||
__slots__ = ()
|
||||
|
||||
position = None
|
||||
always = False
|
||||
def __repr__(self):
|
||||
return "<{} test clause>".format(type(self).__name__.lower())
|
||||
|
||||
|
||||
class PatternException(ValueError):pass
|
||||
|
||||
class Pattern(Clause):
|
||||
__slots__ = ("pattern", "position", "always")
|
||||
|
||||
def __init__(self, loc, pattern=None):
|
||||
Node.__init__(self, loc)
|
||||
super(Pattern, self).__init__(loc)
|
||||
self.pattern = pattern
|
||||
|
||||
def start(self):
|
||||
return True
|
||||
self.position = None
|
||||
self.always = False
|
||||
|
||||
def execute(self, state, t):
|
||||
|
||||
@@ -121,11 +128,10 @@ class Pattern(Node):
|
||||
else:
|
||||
x, y = renpy.test.testfocus.find_position(f, position)
|
||||
|
||||
if x is None:
|
||||
if None in (x, y):
|
||||
if self.pattern:
|
||||
return state
|
||||
else:
|
||||
x, y = renpy.exports.get_mouse_pos()
|
||||
raise PatternException("The given {!r} pattern was not resolved to a target".format(self.pattern))
|
||||
x, y = renpy.exports.get_mouse_pos()
|
||||
|
||||
return self.perform(x, y, state, t)
|
||||
|
||||
@@ -136,16 +142,12 @@ class Pattern(Node):
|
||||
|
||||
f = renpy.test.testfocus.find_focus(self.pattern)
|
||||
|
||||
if f is not None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return (f is not None)
|
||||
|
||||
def perform(self, x, y, state, t):
|
||||
return None
|
||||
|
||||
class Click(Pattern):
|
||||
|
||||
# The number of the button to click.
|
||||
button = 1
|
||||
|
||||
@@ -155,21 +157,18 @@ class Click(Pattern):
|
||||
|
||||
|
||||
class Move(Pattern):
|
||||
|
||||
__slots__ = ()
|
||||
def perform(self, x, y, state, t):
|
||||
move_mouse(x, y)
|
||||
return None
|
||||
|
||||
|
||||
class Scroll(Node):
|
||||
|
||||
class Scroll(Clause):
|
||||
__slots__ = "pattern"
|
||||
def __init__(self, loc, pattern=None):
|
||||
Node.__init__(self, loc)
|
||||
super(Scroll, self).__init__(loc)
|
||||
self.pattern = pattern
|
||||
|
||||
def start(self):
|
||||
return True
|
||||
|
||||
def execute(self, state, t):
|
||||
|
||||
self.report()
|
||||
@@ -206,19 +205,16 @@ class Scroll(Node):
|
||||
return False
|
||||
|
||||
|
||||
class Drag(Node):
|
||||
|
||||
class Drag(Clause):
|
||||
__slots__ = ("points", "pattern", "button", "steps")
|
||||
def __init__(self, loc, points):
|
||||
Node.__init__(self, loc)
|
||||
super(Drag, self).__init__(loc)
|
||||
self.points = points
|
||||
|
||||
self.pattern = None
|
||||
self.button = 1
|
||||
self.steps = 10
|
||||
|
||||
def start(self):
|
||||
return True
|
||||
|
||||
def execute(self, state, t):
|
||||
|
||||
self.report()
|
||||
@@ -241,7 +237,7 @@ class Drag(Node):
|
||||
points = [ renpy.test.testfocus.find_position(f, i) for i in points ]
|
||||
|
||||
if len(points) < 2:
|
||||
raise Exception("A drag requires at least two points.")
|
||||
raise ValueError("A drag requires at least two points.")
|
||||
|
||||
interpoints = [ ]
|
||||
|
||||
@@ -294,8 +290,8 @@ class Drag(Node):
|
||||
|
||||
|
||||
class Type(Pattern):
|
||||
|
||||
interval = .01
|
||||
__slots__ = "keys"
|
||||
# interval = .01 # unused
|
||||
|
||||
def __init__(self, loc, keys):
|
||||
Pattern.__init__(self, loc)
|
||||
@@ -318,10 +314,13 @@ class Type(Pattern):
|
||||
return state + 1
|
||||
|
||||
|
||||
class Action(Node):
|
||||
|
||||
class Action(Clause):
|
||||
"""
|
||||
This is for the `run` keyword
|
||||
"""
|
||||
__slots__ = "expr"
|
||||
def __init__(self, loc, expr):
|
||||
Node.__init__(self, loc)
|
||||
super(Action, self).__init__(loc)
|
||||
self.expr = expr
|
||||
|
||||
def start(self):
|
||||
@@ -345,10 +344,10 @@ class Action(Node):
|
||||
return renpy.display.behavior.is_sensitive(action)
|
||||
|
||||
|
||||
class Pause(Node):
|
||||
|
||||
class Pause(Clause):
|
||||
__slots__ = "expr"
|
||||
def __init__(self, loc, expr):
|
||||
Node.__init__(self, loc)
|
||||
super(Pause, self).__init__(loc)
|
||||
self.expr = expr
|
||||
|
||||
def start(self):
|
||||
@@ -364,34 +363,149 @@ class Pause(Node):
|
||||
return None
|
||||
|
||||
|
||||
class Label(Node):
|
||||
|
||||
class Label(Clause):
|
||||
__slots__ = "name"
|
||||
def __init__(self, loc, name):
|
||||
Node.__init__(self, loc)
|
||||
super(Label, self).__init__(loc)
|
||||
self.name = name
|
||||
|
||||
def start(self):
|
||||
return True
|
||||
|
||||
def execute(self, state, t):
|
||||
if self.name in renpy.test.testexecution.labels:
|
||||
return None
|
||||
else:
|
||||
return state
|
||||
return None
|
||||
|
||||
def ready(self):
|
||||
return self.name in renpy.test.testexecution.labels
|
||||
|
||||
|
||||
class Eval(Clause):
|
||||
__slots__ = ("expr", "evaluated")
|
||||
def __init__(self, loc, expr):
|
||||
super(Eval, self).__init__(loc)
|
||||
self.expr = expr
|
||||
self.evaluated = False
|
||||
|
||||
def execute(self, state, t):
|
||||
if not self.evaluated: # check if this is necessary
|
||||
self.ready()
|
||||
return None
|
||||
|
||||
def ready(self):
|
||||
rv = bool(renpy.python.py_eval(self.expr))
|
||||
self.evaluated = True
|
||||
return rv
|
||||
|
||||
class Pass(Clause):
|
||||
__slots__ = ()
|
||||
def execute(self, state, t):
|
||||
return None
|
||||
|
||||
|
||||
################################################################################
|
||||
# Boolean proxy clauses
|
||||
|
||||
class Not(Clause):
|
||||
__slots__ = "clause"
|
||||
def __init__(self, loc, clause):
|
||||
super(Not, self).__init__(loc)
|
||||
self.clause = clause
|
||||
|
||||
def start(self):
|
||||
return self.clause.start()
|
||||
|
||||
def execute(self, state, t):
|
||||
# return self.clause.execute(state, t)
|
||||
return None
|
||||
|
||||
def ready(self):
|
||||
return not self.clause.ready()
|
||||
|
||||
class Binary(Clause):
|
||||
__slots__ = ("left", "right",
|
||||
"left_ready", "right_ready",
|
||||
"left_state", "right_state")
|
||||
|
||||
def __init__(self, loc, left, right):
|
||||
super(Binary, self).__init__(loc)
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.left_ready = self.right_ready = None
|
||||
|
||||
def start(self):
|
||||
self.left_state = self.left.start()
|
||||
self.right_state = self.right.start()
|
||||
return self.state()
|
||||
|
||||
class And(Binary):
|
||||
__slots__ = ()
|
||||
|
||||
def state(self):
|
||||
if (self.left_state is None) and (self.right_state is None):
|
||||
return None
|
||||
return True
|
||||
|
||||
def execute(self, state, t):
|
||||
"""
|
||||
Executes both if both are ready, otherwise the left one.
|
||||
"""
|
||||
self.ready()
|
||||
|
||||
if self.left_state is not None:
|
||||
self.left_state = self.left.execute(self.left_state, t)
|
||||
|
||||
if self.left_ready and self.right_ready and (self.right_state is not None):
|
||||
self.right_state = self.right.execute(self.right_state, t)
|
||||
|
||||
return self.state()
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Memorizes the computed values.
|
||||
Effectively returns self.left.ready() and self.right.ready().
|
||||
"""
|
||||
self.left_ready = self.left.ready()
|
||||
self.right_ready = self.right.ready()
|
||||
return self.left_ready and self.right_ready
|
||||
|
||||
class Or(Binary):
|
||||
__slots__ = ()
|
||||
|
||||
def state(self):
|
||||
if (self.left_state is None) or (self.right_state is None):
|
||||
return None
|
||||
return True
|
||||
|
||||
def execute(self, state, t):
|
||||
"""
|
||||
Executes the ready one(s), if any, otherwise the right one.
|
||||
"""
|
||||
self.ready()
|
||||
|
||||
if self.left_ready and (self.left_state is not None):
|
||||
self.left_state = self.left.execute(self.left_state, t)
|
||||
|
||||
if (self.right_ready or not self.left_ready) and (self.right_state is not None):
|
||||
self.right_state = self.right.execute(self.right_state, t)
|
||||
|
||||
return self.state()
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Memorizes the computed values.
|
||||
Effectively returns self.left.ready() or self.right.ready().
|
||||
"""
|
||||
self.left_ready = self.left.ready()
|
||||
self.right_ready = self.right.ready()
|
||||
return self.left_ready or self.right_ready
|
||||
|
||||
|
||||
################################################################################
|
||||
# Non-clause statements.
|
||||
|
||||
class Until(Node):
|
||||
"""
|
||||
Executes `left` repeatedly until `right` is ready, then executes `right`
|
||||
once before quitting.
|
||||
Executes `left` repeatedly until `right` is ready (and unless it already is),
|
||||
then executes `right` once before quitting.
|
||||
"""
|
||||
|
||||
__slots__ = ("left", "right")
|
||||
def __init__(self, loc, left, right):
|
||||
Node.__init__(self, loc)
|
||||
self.left = left
|
||||
@@ -403,7 +517,7 @@ class Until(Node):
|
||||
def execute(self, state, t):
|
||||
child, child_state, start = state
|
||||
|
||||
if self.right.ready() and not (child is self.right):
|
||||
if self.right.ready() and (child is not self.right):
|
||||
child = self.right
|
||||
child_state = None
|
||||
|
||||
@@ -431,7 +545,7 @@ class If(Node):
|
||||
If `condition` is ready, runs the block. Otherwise, goes to the next
|
||||
statement.
|
||||
"""
|
||||
|
||||
__slots__ = ("condition", "block")
|
||||
def __init__(self, loc, condition, block):
|
||||
Node.__init__(self, loc)
|
||||
|
||||
@@ -459,10 +573,11 @@ class If(Node):
|
||||
|
||||
|
||||
class Python(Node):
|
||||
|
||||
def __init__(self, loc, code):
|
||||
__slots__ = ("code", "hide")
|
||||
def __init__(self, loc, code, hide=False):
|
||||
Node.__init__(self, loc)
|
||||
self.code = code
|
||||
self.hide = hide
|
||||
|
||||
def start(self):
|
||||
renpy.test.testexecution.action = self
|
||||
@@ -478,35 +593,31 @@ class Python(Node):
|
||||
return None
|
||||
|
||||
def __call__(self):
|
||||
renpy.python.py_exec_bytecode(self.code.bytecode)
|
||||
renpy.python.py_exec_bytecode(self.code.bytecode, self.hide)
|
||||
|
||||
|
||||
class AssertError(AssertionError):pass
|
||||
|
||||
class Assert(Node):
|
||||
|
||||
def __init__(self, loc, expr):
|
||||
__slots__ = "clause"
|
||||
def __init__(self, loc, clause):
|
||||
Node.__init__(self, loc)
|
||||
self.expr = expr
|
||||
|
||||
def start(self):
|
||||
renpy.test.testexecution.action = self
|
||||
return True
|
||||
self.clause = clause
|
||||
|
||||
def execute(self, state, t):
|
||||
|
||||
self.report()
|
||||
|
||||
if renpy.test.testexecution.action:
|
||||
return True
|
||||
else:
|
||||
return None
|
||||
if not self.clause.ready():
|
||||
raise AssertError("On line {}:{}, assertion of {} failed.".format(self.filename,
|
||||
self.linenumber,
|
||||
self.clause))
|
||||
|
||||
def __call__(self):
|
||||
if not renpy.python.py_eval(self.expr):
|
||||
raise Exception("On line {}:{}, assertion {} failed.".format(self.filename, self.linenumber, self.expr))
|
||||
return None
|
||||
|
||||
|
||||
class Jump(Node):
|
||||
|
||||
__slots__ = "target"
|
||||
def __init__(self, loc, target):
|
||||
Node.__init__(self, loc)
|
||||
|
||||
@@ -518,7 +629,7 @@ class Jump(Node):
|
||||
|
||||
|
||||
class Call(Node):
|
||||
|
||||
__slots__ = "target"
|
||||
def __init__(self, loc, target):
|
||||
Node.__init__(self, loc)
|
||||
|
||||
@@ -544,7 +655,7 @@ class Call(Node):
|
||||
# Control structures.
|
||||
|
||||
class Block(Node):
|
||||
|
||||
__slots__ = "block"
|
||||
def __init__(self, loc, block):
|
||||
Node.__init__(self, loc)
|
||||
self.block = block
|
||||
@@ -569,3 +680,8 @@ class Block(Node):
|
||||
i += 1
|
||||
|
||||
return i, start, s
|
||||
|
||||
class Exit(Node):
|
||||
__slots__ = ()
|
||||
def execute(self, state, t):
|
||||
raise renpy.game.QuitException
|
||||
|
||||
@@ -86,7 +86,7 @@ def lookup(name, from_node):
|
||||
if name in testcases:
|
||||
return testcases[name]
|
||||
|
||||
raise Exception("Testcase {} not found at {}:{}.".format(name, from_node.filename, from_node.linenumber))
|
||||
raise KeyError("Testcase {} not found at {}:{}.".format(name, from_node.filename, from_node.linenumber))
|
||||
|
||||
|
||||
def execute_node(now, node, state, start):
|
||||
@@ -162,6 +162,8 @@ def execute():
|
||||
|
||||
if node is None:
|
||||
renpy.test.testmouse.reset()
|
||||
for clbk in renpy.config.end_testcase_callbacks:
|
||||
clbk()
|
||||
return
|
||||
|
||||
loc = renpy.exports.get_filename_line()
|
||||
@@ -196,4 +198,4 @@ def test_command():
|
||||
return True
|
||||
|
||||
|
||||
renpy.arguments.register_command("test", test_command)
|
||||
renpy.arguments.register_command("test", test_command, uses_display=True)
|
||||
|
||||
+21
-25
@@ -35,13 +35,19 @@ def find_focus(pattern):
|
||||
If `pattern` is None, returns a random coordinate that will trigger the
|
||||
default focus.
|
||||
|
||||
If `pattern` could not be found, returns None, None.
|
||||
If `pattern` could not be found, returns None.
|
||||
"""
|
||||
|
||||
if pattern is not None:
|
||||
pattern = pattern.casefold()
|
||||
|
||||
def match(f):
|
||||
|
||||
if pattern is None:
|
||||
if f.x is None:
|
||||
# one focus, at most, ever branches here
|
||||
# the main "click to continue" one
|
||||
# it's the only one, if any, to be retained in the matching list
|
||||
return "default"
|
||||
else:
|
||||
return None
|
||||
@@ -51,38 +57,34 @@ def find_focus(pattern):
|
||||
else:
|
||||
t = f.widget._tts_all()
|
||||
|
||||
if pattern.lower() in t.lower():
|
||||
if pattern in t.casefold():
|
||||
return t
|
||||
else:
|
||||
return None
|
||||
|
||||
# A list of alt_text, focus pairs.
|
||||
matching = [ ]
|
||||
# {focus : (len(alt), alt)}
|
||||
matching = {}
|
||||
|
||||
for f in renpy.display.focus.focus_list:
|
||||
|
||||
alt = match(f)
|
||||
|
||||
if alt is not None:
|
||||
matching.append((alt, f))
|
||||
|
||||
if not matching:
|
||||
return None
|
||||
matching[f] = (len(alt), alt)
|
||||
|
||||
# This gets the matching displayable with the shortest alt text, which
|
||||
# is likely what we want.
|
||||
matching.sort(key=lambda a : (len(a[0]), a[0]))
|
||||
return matching[0][1]
|
||||
return min(matching, key=matching.get, default=None) # type: ignore
|
||||
|
||||
|
||||
def relative_position(x, posx, width):
|
||||
if posx is not None:
|
||||
if isinstance(posx, float):
|
||||
x = int(posx * (width - 1))
|
||||
if type(posx) is float:
|
||||
x = posx * (width - 1)
|
||||
else:
|
||||
x = posx
|
||||
|
||||
return int(x)
|
||||
return x
|
||||
|
||||
|
||||
def find_position(f, position):
|
||||
@@ -105,8 +107,8 @@ def find_position(f, position):
|
||||
|
||||
if f is None:
|
||||
return (
|
||||
relative_position(x, posx, renpy.config.screen_width),
|
||||
relative_position(y, posy, renpy.config.screen_height),
|
||||
int(relative_position(x, posx, renpy.config.screen_width)),
|
||||
int(relative_position(y, posy, renpy.config.screen_height)),
|
||||
)
|
||||
|
||||
orig_f = f
|
||||
@@ -119,13 +121,10 @@ def find_position(f, position):
|
||||
f.w = renpy.config.screen_width
|
||||
f.h = renpy.config.screen_height
|
||||
|
||||
x = relative_position(x, posx, f.w) + f.x
|
||||
y = relative_position(y, posy, f.h) + f.y
|
||||
x = int(relative_position(x-f.x, posx, f.w) + f.x)
|
||||
y = int(relative_position(y-f.y, posy, f.h) + f.y)
|
||||
|
||||
for _i in range(100):
|
||||
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
for _i in range(renpy.test.testast._test.focus_trials):
|
||||
|
||||
nf = renpy.display.render.focus_at_point(x, y)
|
||||
|
||||
@@ -139,7 +138,4 @@ def find_position(f, position):
|
||||
x = random.randrange(f.x, f.x + f.w)
|
||||
y = random.randrange(f.y, f.y + f.h)
|
||||
|
||||
else:
|
||||
print()
|
||||
|
||||
raise Exception("Could not locate the displayable.")
|
||||
raise Exception("Could not locate the displayable.")
|
||||
|
||||
@@ -39,7 +39,7 @@ def parse_click(l, loc, target):
|
||||
elif l.keyword('pos'):
|
||||
rv.position = l.require(l.simple_expression)
|
||||
|
||||
elif l.keyword('always'):
|
||||
elif (target is not None) and l.keyword('always'):
|
||||
rv.always = True
|
||||
|
||||
else:
|
||||
@@ -103,8 +103,31 @@ def parse_drag(l, loc):
|
||||
return rv
|
||||
|
||||
|
||||
def parse_not(l, loc):
|
||||
if l.keyword("not"):
|
||||
return testast.Not(loc, parse_not(l, loc))
|
||||
else:
|
||||
return parse_clause(l, loc)
|
||||
|
||||
def parse_and(l, loc):
|
||||
rv = parse_not(l, loc)
|
||||
while l.keyword("and"):
|
||||
rv = testast.And(loc, rv, parse_not(l, loc))
|
||||
return rv
|
||||
|
||||
def parse_or(l, loc):
|
||||
rv = parse_and(l, loc)
|
||||
while l.keyword("or"):
|
||||
rv = testast.Or(loc, rv, parse_and(l, loc))
|
||||
return rv
|
||||
|
||||
def parse_clause(l, loc):
|
||||
if l.keyword("run"):
|
||||
if l.match(r'\('):
|
||||
rv = parse_or(l, loc)
|
||||
l.require(r'\)')
|
||||
return rv
|
||||
|
||||
elif l.keyword("run"):
|
||||
|
||||
expr = l.require(l.simple_expression)
|
||||
return testast.Action(loc, expr)
|
||||
@@ -119,6 +142,11 @@ def parse_clause(l, loc):
|
||||
name = l.require(l.name)
|
||||
return testast.Label(loc, name)
|
||||
|
||||
elif l.keyword('eval'):
|
||||
|
||||
source = l.require(l.simple_expression)
|
||||
return testast.Eval(loc, source)
|
||||
|
||||
elif l.keyword("type"):
|
||||
|
||||
name = l.name()
|
||||
@@ -143,6 +171,9 @@ def parse_clause(l, loc):
|
||||
pattern = l.require(l.string)
|
||||
return testast.Scroll(loc, pattern)
|
||||
|
||||
elif l.keyword("pass"):
|
||||
return testast.Pass(loc)
|
||||
|
||||
else:
|
||||
target = l.string()
|
||||
if target:
|
||||
@@ -156,14 +187,16 @@ def parse_statement(l, loc):
|
||||
|
||||
if l.keyword('python'):
|
||||
|
||||
hide = l.keyword('hide')
|
||||
|
||||
l.require(':')
|
||||
|
||||
l.expect_block("python block")
|
||||
|
||||
source = l.python_block()
|
||||
|
||||
code = renpy.ast.PyCode(source, loc)
|
||||
return testast.Python(loc, code)
|
||||
code = renpy.ast.PyCode(source, loc, 'hide' if hide else 'exec')
|
||||
return testast.Python(loc, code, hide)
|
||||
|
||||
if l.keyword("if"):
|
||||
l.expect_block("if block")
|
||||
@@ -186,8 +219,8 @@ def parse_statement(l, loc):
|
||||
return testast.Python(loc, code)
|
||||
|
||||
elif l.keyword('assert'):
|
||||
source = l.require(l.rest)
|
||||
return testast.Assert(loc, source)
|
||||
check = parse_clause(l, loc)
|
||||
return testast.Assert(loc, check)
|
||||
|
||||
elif l.keyword('jump'):
|
||||
target = l.require(l.name)
|
||||
@@ -197,6 +230,9 @@ def parse_statement(l, loc):
|
||||
target = l.require(l.name)
|
||||
return testast.Call(loc, target)
|
||||
|
||||
elif l.keyword('exit'):
|
||||
return testast.Exit(loc)
|
||||
|
||||
rv = parse_clause(l, loc)
|
||||
|
||||
if l.keyword("until"):
|
||||
|
||||
@@ -1350,6 +1350,14 @@ Rarely or Internally Used
|
||||
controller GUID (which cand be found in log.txt), and if matched,
|
||||
prevent the controller from being initialized.
|
||||
|
||||
.. var:: config.end_testcase_callbacks = [ ... ]
|
||||
|
||||
A list of functions that are called (without any arguments) when
|
||||
execution of testcases end - in a good way or not. This is intended
|
||||
to free resources as well as cleaning up anything that may have been
|
||||
modified for the purpose of the test, such as persistent or save
|
||||
data.
|
||||
|
||||
.. var:: config.exception_handler = None
|
||||
|
||||
If not None, this should be a function that takes three arguments:
|
||||
|
||||
@@ -84,6 +84,7 @@ To find out more about Ren'Py, please visit the Ren'Py home page:
|
||||
launcher
|
||||
developer_tools
|
||||
director
|
||||
testcases
|
||||
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -62,6 +62,7 @@ behavior:
|
||||
default store, the Python will execute in the store with that
|
||||
name.
|
||||
|
||||
.. _dollar-line:
|
||||
|
||||
One-line Python Statement
|
||||
-------------------------
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
=====================
|
||||
Integrated test suite
|
||||
=====================
|
||||
|
||||
Ren'py allows creators to put tests in their games to make sure that an
|
||||
alteration to the code didn't break the game.
|
||||
|
||||
Testcase statement
|
||||
==================
|
||||
|
||||
The ``testcase`` statement creates a named testcase, it takes a name, a colon,
|
||||
and a block which contains successive test statements (see below). Testcases are
|
||||
similar to Ren'py :ref:`labels <label-statement>`, with a few specificities:
|
||||
|
||||
- The Testcase statement takes test statements, the Ren'py label statement takes
|
||||
Ren'py code. Both are mutually excusive.
|
||||
- There is no testcase equivalent of the return statement: like in a python
|
||||
block, the removal of indentation and the end of the block closes the testcase.
|
||||
- There can be no test statement outside of a testcase block, while there can be
|
||||
Ren'py code oustside labels (in init blocks for example).
|
||||
|
||||
::
|
||||
|
||||
testcase default:
|
||||
"Start"
|
||||
exit
|
||||
|
||||
The testcase named ``default`` will be executed first - the other ones may be
|
||||
called by the default testcase.
|
||||
|
||||
Note that when a testcase finishes executing, the game doesn't close itself
|
||||
(unless the ``exit`` statement is used). Instead, it will just remain where it
|
||||
ended up in the game, awaiting user action.
|
||||
|
||||
When that happens, or if the game quits before that happens (because of an
|
||||
``exit``, because of an exception, or for another reason), the functions added
|
||||
to the :var:`config.end_testcase_callbacks` list are called without arguments.
|
||||
This allows for clean-up code to be executed whatever may happen during the
|
||||
tests.
|
||||
|
||||
.. should an exception during a callback call prevent subsequent callbacks from being called ?
|
||||
|
||||
There is no equivalent callback for code being executed before the tests
|
||||
happen, as such code can simply be put inside a python block at the beginning
|
||||
of the testcase, or in ``init python`` blocks.
|
||||
|
||||
The :func:`renpy.is_in_test` function is helpful to know whether a test is currently
|
||||
executing or not.
|
||||
|
||||
Test statements
|
||||
===============
|
||||
|
||||
The following statements can be written in ``testcase`` blocks.
|
||||
|
||||
python blocks and dollar-lines
|
||||
------------------------------
|
||||
|
||||
A :ref:`python block <python-statement>` or a :ref:`dollar-line` can be added
|
||||
within a testcase. Unlike in normal Ren'py code, the python blocks don't take
|
||||
the ``in substore`` parameter, but it does take the ``hide`` keyword. They
|
||||
(both) allow execution of arbitrary python code.
|
||||
|
||||
Init code gets executed before the test occurs, so functions and classes defined
|
||||
in ``init python`` blocks can be called in test python blocks and in test
|
||||
dollar-lines. For example::
|
||||
|
||||
init python in test:
|
||||
def afunction():
|
||||
if renpy.is_in_test():
|
||||
return "test"
|
||||
return "not test"
|
||||
|
||||
testcase default:
|
||||
$ print(test.afunction()) # ends up in the console
|
||||
|
||||
if statement
|
||||
------------
|
||||
|
||||
This statement, like a python ``if`` statement, takes a block.
|
||||
|
||||
Unlike the Ren'py or python versions however, it only takes a
|
||||
:ref:`test clause <test-clauses>`, instead of a general python expression.
|
||||
The provided clause being ready is the actual condition for the block to execute
|
||||
or not.
|
||||
|
||||
Example::
|
||||
|
||||
if label "chapter_five":
|
||||
exit
|
||||
|
||||
A normal python/renpy ``if`` can be replicated using the :ref:`test-eval-clause`\ ::
|
||||
|
||||
if eval (persistent.should_advance and
|
||||
i_should_advance["now"]):
|
||||
click
|
||||
|
||||
.. there is no elif nor else clause
|
||||
|
||||
assert statement
|
||||
----------------
|
||||
|
||||
Similarly to a python assert, this statement takes a :ref:`clause <test-clauses>`
|
||||
and raises an AssertError if the clause is not ready at the time when the assert
|
||||
statement executes.
|
||||
|
||||
A python assert can be replicated with::
|
||||
|
||||
assert eval some_function(args)
|
||||
|
||||
About python assert statements, see the python documentation
|
||||
`regarding asserts <https://docs.python.org/reference/simple_stmts.html#the-assert-statement>`__
|
||||
and
|
||||
`about boolean evaluation <https://docs.python.org/library/stdtypes.html#truth-value-testing>`__.
|
||||
|
||||
.. note::
|
||||
|
||||
The regular ``assert`` python statement is not guaranteed to work in Ren'py.
|
||||
Notably, it was disabled in version 7, and in some older versions.
|
||||
|
||||
Therefore, the following may not actually check what it's supposed to check::
|
||||
|
||||
$ assert some_function(args)
|
||||
|
||||
In some versions of Ren'py or depending on unpredictable conditions,
|
||||
``some_function`` may not even get called. The assert test statement should
|
||||
be used instead.
|
||||
|
||||
jump statement
|
||||
--------------
|
||||
|
||||
call statement
|
||||
--------------
|
||||
|
||||
.. reminder that there is no return statement in testcases
|
||||
|
||||
.. to jump to a renpy label, use the run clause:: run Jump("label_name")
|
||||
|
||||
clause statement
|
||||
----------------
|
||||
|
||||
A :ref:`clause <test-clauses>` can be given, just by itself. ::
|
||||
|
||||
pause 5
|
||||
click
|
||||
|
||||
until statement
|
||||
---------------
|
||||
|
||||
This statement consists in two :ref:`clauses <test-clauses>`, separated by the
|
||||
word ``until``.
|
||||
|
||||
If and when the clause on the right is ready, it is executed and control is
|
||||
passed to the next statement. Otherwise, the left clause is executed until the
|
||||
right clause is ready, and then the right clause is executed.
|
||||
|
||||
This is basically an inline while loop. ::
|
||||
|
||||
click until eval renpy.get_screen("choice")
|
||||
"ask her right"
|
||||
click until "It's an interactive book."
|
||||
|
||||
exit statement
|
||||
--------------
|
||||
|
||||
Quits the game without calling the confirmation screen.
|
||||
Does not save the game when quitting::
|
||||
|
||||
if eval need_to_confirm:
|
||||
run Quit(confirm=True) # asks for confirmation, and autosaves
|
||||
|
||||
if eval persistent.quit_test_using_action:
|
||||
run Quit(confirm=False) # does not ask, but still autosaves
|
||||
|
||||
exit # neither asks nor autosaves
|
||||
|
||||
.. _test-clauses:
|
||||
|
||||
Test clauses
|
||||
============
|
||||
|
||||
Clauses are defined by two things: what it does when they are executed, and
|
||||
under what circumstances they are ready. They can be part of ``if``, ``assert``
|
||||
or ``until`` test statements, or they can simply be given on their own (see
|
||||
above).
|
||||
|
||||
.. for each one, say what makes it ready
|
||||
|
||||
pass clause
|
||||
--------------
|
||||
|
||||
Does not do anything. It's a no-op, allowing for empty testcases. ::
|
||||
|
||||
testcase not_yet_implemented:
|
||||
pass
|
||||
|
||||
It is always ready.
|
||||
|
||||
click clause
|
||||
---------------
|
||||
|
||||
Executes a simulated click on the screen. It takes the following optional
|
||||
properties:
|
||||
|
||||
- ``button`` specifies which button of the simulated mouse is to be clicked
|
||||
with. It takes an integer and defaults to 1. 1 is a left-click, 2 is a
|
||||
right-click, 3 is a scrollwheel-click, 4 and 5 are additional buttons found on
|
||||
some mouses. Normally only 1 and 2 trigger any response from Ren'py.
|
||||
- ``pos`` specifies where to click, as a pair of x/y coordinates. Coordinates
|
||||
are taken relative to the screen. Floats between 0.0 and 1.0 are supported as
|
||||
a fraction of the screen size in either dimension. ``absolute`` and other
|
||||
means of expressing positions are not supported, since you can't move the
|
||||
mouse to a subpixel-precise position.
|
||||
|
||||
.. ``always`` is not documented because useless in the case of the click clause by itself
|
||||
|
||||
Click behaves like a :ref:`pattern <test-pattern>`\ -taking clause which would
|
||||
not be given a pattern: if no ``pos`` is provided, it will look for a neutral
|
||||
place where a click would not occur on a focusable element.
|
||||
|
||||
.. give example for both
|
||||
|
||||
This clause is always ready.
|
||||
|
||||
The :func:`has_default_focus` function is a helpful accessor to know whether a
|
||||
game can be advanced by a bare ``click`` clause or not::
|
||||
|
||||
click until eval (not renpy.has_default_focus())
|
||||
|
||||
string expression clause
|
||||
------------------------
|
||||
|
||||
This clause consists in a simple string, which is interpreted as a
|
||||
:ref:`pattern <test-pattern>`. It executes by simulating a click on the target
|
||||
identified by the pattern.
|
||||
|
||||
It takes three optional properties:
|
||||
|
||||
- ``button`` - same as the click clause
|
||||
- ``pos`` - same as the click clause, but the position is relative to the
|
||||
focusable area of the target. If the position is ommitted or is invalid, for example if a
|
||||
button is 100x100 pixels and the given ``pos`` is (105, 150), then the ``pos``
|
||||
is ignored and a random position within the target is used instead.
|
||||
- ``always`` does not take a value. It overrides the readiness of the clause,
|
||||
making it always ready.
|
||||
|
||||
This clause is ready if and when its pattern resolves to a suitable target found
|
||||
on the screen, or if it is given the ``always`` property. ::
|
||||
|
||||
testcase play_the_game:
|
||||
"start"
|
||||
|
||||
run clause
|
||||
-------------
|
||||
|
||||
Runs the provided :doc:`screen-language action <screen_actions>` (or list of
|
||||
actions).
|
||||
|
||||
Ready if and when a button containing the provided action (or list) would be
|
||||
sensitive. ::
|
||||
|
||||
testcase chapter_3:
|
||||
run Jump("chapter_3")
|
||||
|
||||
pause clause
|
||||
---------------
|
||||
|
||||
Pauses test execution for a given number of seconds.
|
||||
|
||||
This clause is always ready. ::
|
||||
|
||||
pause 5.0
|
||||
|
||||
Similar to the :ref:`pause-statement`, but requires a value (there is no
|
||||
click-to-continue pause in tests).
|
||||
|
||||
label clause
|
||||
---------------
|
||||
|
||||
Does not do anything when executed. This clause only exists to be used inside
|
||||
clause-taking test statements like ``assert``, ``if`` or ``until``.
|
||||
|
||||
The label clause is ready if and when the provided label has been passed between
|
||||
the current test statement and the one just before.
|
||||
|
||||
Attention, this means that the following example does not work::
|
||||
|
||||
"play chapter 1"
|
||||
# passing the "chapter_1" label
|
||||
pause 1
|
||||
assert label chapter_1
|
||||
|
||||
It will not work because no renpy label will have been reached between the
|
||||
statement containing the label clause and the preceding statement, which in this
|
||||
case are the assert statement and the pause statement, respectively. The same
|
||||
happens in the following example::
|
||||
|
||||
"play chapter 1"
|
||||
# passing the "chapter_1" label
|
||||
assert label chapter_1 # works
|
||||
assert label chapter_1 # fails
|
||||
|
||||
The chapter_1 label is not reached between the first label clause and the second
|
||||
label clause, therefore the second label clause fails (technically, the clause
|
||||
is not ready and the assert fails).
|
||||
|
||||
In both examples, the assert label statement would have worked if it were placed
|
||||
on its own, directly after the ``"play chapter 1"`` string expression statement
|
||||
(or after the comment, which doesn't count as a statement)::
|
||||
|
||||
"play chapter 1"
|
||||
# passing the "chapter_1" label
|
||||
assert label chapter_1
|
||||
# all fine
|
||||
|
||||
.. warning::
|
||||
|
||||
This clause should not be confused with the Ren'py native
|
||||
:ref:`label <label-statement>` statement it refers to, or with the unrelated
|
||||
:ref:`screen-language label element <sl-label>`.
|
||||
|
||||
drag clause
|
||||
--------------
|
||||
|
||||
..
|
||||
simulate the mouse dragging something from one place to another
|
||||
by maintaining click blabla
|
||||
takes an iterable of points to follow as an itinerary
|
||||
each point must be given as a pair of x/y coordinates, or None
|
||||
each occurrence of None will be replaced with a coordinate within the focused area of the screen
|
||||
(the position of the virtual test mouse if already inside it, or a random position within if not)
|
||||
needs to be given at least two points
|
||||
ready if the thing it has been told to type in is found, or if no target has been given
|
||||
show example of ((None, 10), (None, 100)) being an only-vertical movement downwards
|
||||
|
||||
.. warning disambiguation ? probably not necessary
|
||||
|
||||
scroll clause
|
||||
----------------
|
||||
|
||||
..
|
||||
takes a string giving it a pattern
|
||||
ready when the target (pattern) is found
|
||||
If the target is a bar, scrolls it down a page. If already at the bottom, returns it to the top.
|
||||
|
||||
.. _test-eval-clause:
|
||||
|
||||
eval clause
|
||||
-----------
|
||||
|
||||
This clause is ready if and when the provided expression evaluates to a true
|
||||
value, in a boolean context.
|
||||
|
||||
When executed, does not do anything other than evaluating the expression it is
|
||||
given. This clause exists only to be used inside clause-taking test statements
|
||||
like ``assert``, ``if`` or ``until``, effectively turning ``assert`` and ``if``
|
||||
into their non-clause-taking python equivalents::
|
||||
|
||||
assert eval (renpy.is_in_test() and
|
||||
("Ren'py" in renpy.version_string))
|
||||
|
||||
.. note::
|
||||
|
||||
Differences between a dollar-line and the eval clause :
|
||||
|
||||
- A dollar-line executes any python statement, which does not necessarily
|
||||
have a value - for example ``$ import math`` - while the eval clause
|
||||
requires an expression, a.k.a something having a value.
|
||||
- The eval clause provides a value to an ``if`` or ``until`` statement,
|
||||
while these statements can't take a dollar sign, much less a dollar-line.
|
||||
|
||||
..
|
||||
|
||||
When the returned value of a function call is to be ignored, both are technically equivalent::
|
||||
|
||||
$ print("Test 1")
|
||||
eval print("Test 2")
|
||||
|
||||
This is because functions always return a value (None being a value), unless they raise an exception.
|
||||
|
||||
type clause
|
||||
--------------
|
||||
|
||||
.. simulate a key-pressing or the typing of text
|
||||
|
||||
..
|
||||
It is ready if a pattern is not provided,
|
||||
or if one is provided and a suitable target is found on the screen.
|
||||
For the clauses taking the ``always`` property, that property overrides the readiness of the clause.
|
||||
|
||||
..
|
||||
warning disambiguation this has nothing to do with the python builtin
|
||||
|
||||
move clause
|
||||
--------------
|
||||
|
||||
..
|
||||
`move (position) [pattern (string)]`
|
||||
moves the virtual test mouse to the provided position, within the area targeted by the pattern
|
||||
or, if none is given, within the whole screen
|
||||
|
||||
..
|
||||
It is ready if a pattern is not provided,
|
||||
or if one is provided and a suitable target is found on the screen.
|
||||
For the clauses taking the ``always`` property, that property overrides the readiness of the clause.
|
||||
|
||||
Boolean clause operations
|
||||
-------------------------
|
||||
|
||||
Test clauses support the ``not``, ``and`` and ``or`` operators: an expression
|
||||
containing clauses linked with these operators can be provided in lieu of a
|
||||
single clause. That expression must always be enclosed in parentheses.
|
||||
|
||||
The readiness of a boolean clause expression is the computation of the readiness
|
||||
of the clauses it contains:
|
||||
- ``(not a)`` is ready if and when ``a`` is not ready
|
||||
- ``(a and b)`` is ready when both ``a`` and ``b`` are ready
|
||||
- ``(a or b)`` is ready when either ``a`` or ``b`` is ready.
|
||||
|
||||
What happens when boolean clause operations execute is a little more complex.
|
||||
When executed:
|
||||
- ``not`` doesn't do anything.
|
||||
- ``and`` executes both clauses if both are ready, and the left one otherwise.
|
||||
- ``or`` executes its ready clause(s), if any, and the right one otherwise. (TODO : maybe it should execute the ready clause if only one is, and the right one otherwise)
|
||||
|
||||
More information can be found in the python documentation
|
||||
`regarding these operators <https://docs.python.org/3/reference/expressions.html#boolean-operations>`__
|
||||
as for why it works that way. The readiness and the behavior of such expressions
|
||||
when executed can be inferred by replacing each clause in the expression with
|
||||
its respective readiness, and working out which of the clause is the result of
|
||||
the operation.
|
||||
|
||||
.. _test-pattern:
|
||||
|
||||
Patterns
|
||||
===============
|
||||
|
||||
Some clauses take a pattern, which helps positioning the mouse or locating where
|
||||
a clause will do what it does.
|
||||
|
||||
The ``pattern`` property takes a string (except in the case of the string
|
||||
expression clause, where it is the string itself) which resolves to a target
|
||||
found on the screen, based on the shortest match among the alt text of focusable
|
||||
screen elements (typically, buttons). The search is case-insensitive.
|
||||
|
||||
If no pattern is given, the virtual test mouse is positioned to the last
|
||||
previous location where a click happened, or to the specified position, if any.
|
||||
If that position lies on a focusable element, a random position in the screen
|
||||
which does not overlap a focusable element is chosen instead.
|
||||
|
||||
If a pattern is given, the mouse is positioned to the last previous location
|
||||
where a click happened, or to the specified position, if any. If that position
|
||||
does not lie inside the targeted element, a random position within it is chosen
|
||||
instead. To that end, things like :propref:`focus_mask` are taken into account.
|
||||
|
||||
If a pattern is given and if it does not resolve to a target at the time when
|
||||
the clause using it executes, an exception is raised (terminating the test). To
|
||||
test whether a given pattern resolves to a target at a given time, the readiness
|
||||
condition of a string expression clause can be evaluated inside an if statement::
|
||||
|
||||
if "ask her right": # if there is a focusable element containing that text on screen
|
||||
# add a clause using that pattern
|
||||
@@ -0,0 +1,6 @@
|
||||
testcase default:
|
||||
"start"
|
||||
click until "ask her right away"
|
||||
click until ("an interactive book" or "video game")
|
||||
click until eval (not renpy.has_default_focus())
|
||||
"quit"
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
testcase player_experience:
|
||||
testcase player_experience:
|
||||
scroll "Bar" until "Player Experience"
|
||||
click until "Yes."
|
||||
|
||||
@@ -198,6 +197,9 @@ testcase translations:
|
||||
click until label tutorials
|
||||
|
||||
|
||||
init python:
|
||||
import time
|
||||
|
||||
testcase out_of_game:
|
||||
"Back"
|
||||
"Back"
|
||||
@@ -210,6 +212,8 @@ testcase out_of_game:
|
||||
$ _preferences.afm_time = 1
|
||||
|
||||
|
||||
$ testvar = time.time()
|
||||
|
||||
"Auto"
|
||||
scroll "Bar" until "Player Experience"
|
||||
"Auto"
|
||||
@@ -218,8 +222,12 @@ testcase out_of_game:
|
||||
pause .5
|
||||
|
||||
"Save"
|
||||
pause .5
|
||||
|
||||
"Save Slot 1"
|
||||
"Yes"
|
||||
("Yes" or pass)
|
||||
|
||||
$ testvar = "value not comparable with a number"
|
||||
|
||||
"Load"
|
||||
pause .5
|
||||
@@ -227,6 +235,8 @@ testcase out_of_game:
|
||||
"Load Slot 1"
|
||||
"Yes"
|
||||
|
||||
assert eval (time.time() - testvar < 15)
|
||||
|
||||
"Prefs"
|
||||
pause .5
|
||||
|
||||
@@ -294,6 +304,3 @@ testcase default:
|
||||
"That's enough for now."
|
||||
click until "Quit"
|
||||
pause .5
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user