1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
2#
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation files
5# (the "Software"), to deal in the Software without restriction,
6# including without limitation the rights to use, copy, modify, merge,
7# publish, distribute, sublicense, and/or sell copies of the Software,
8# and to permit persons to whom the Software is furnished to do so,
9# subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be
12# included in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
23from renpy.compat import *
24
25import renpy.display
26import renpy.test
27import pygame_sdl2
28
29# A map from the name of a testcase to the testcase.
30testcases = { }
31
32# The root node.
33node = None
34
35# The location of the currently execution TL node.
36node_loc = None
37
38# The state of the root node.
39state = None
40
41# The previous state and location in the game script.
42old_state = None
43old_loc = None
44
45# The last time the state changed.
46last_state_change = 0
47
48# The time the root node started executing.
49start_time = None
50
51# An action to run before executing another command.
52action = None
53
54# The set of labels that have been reached since the last time execute
55# has been called.
56labels = set()
57
58
59def take_name(name):
60    """
61    Takes the name of a statement that is about to run.
62    """
63
64    if node is None:
65        return
66
67    if isinstance(name, basestring):
68        labels.add(name)
69
70
71class TestJump(Exception):
72    """
73    An exception that is raised in order to jump to `node`.
74    """
75
76    def __init__(self, node):
77        self.node = node
78
79
80def lookup(name, from_node):
81    """
82    Tries to look up the name with `target`. If found, returns it, otherwise
83    raises an exception.
84    """
85
86    if name in testcases:
87        return testcases[name]
88
89    raise Exception("Testcase {} not found at {}:{}.".format(name, from_node.filename, from_node.linenumber))
90
91
92def execute_node(now, node, state, start):
93    """
94    Performs one execution cycle of a node.
95    """
96
97    while True:
98
99        try:
100            if state is None:
101                state = node.start()
102                start = now
103
104            if state is None:
105                break
106
107            state = node.execute(state, now - start)
108
109            break
110
111        except TestJump as e:
112            node = e.node
113            state = None
114
115    if state is None:
116        node = None
117
118    return node, state, start
119
120
121def execute():
122    """
123    Called periodically by the test code to generate events, if desired.
124    """
125
126    global node
127    global state
128    global start_time
129    global action
130    global old_state
131    global old_loc
132    global last_state_change
133
134    _test = renpy.test.testast._test
135
136    if node is None:
137        return
138
139    if renpy.display.interface.suppress_underlay and (not _test.force):
140        return
141
142    if _test.maximum_framerate:
143        renpy.exports.maximum_framerate(10.0)
144    else:
145        renpy.exports.maximum_framerate(None)
146
147    # Make sure there are no test events in the event queue.
148    for e in pygame_sdl2.event.copy_event_queue():  # @UndefinedVariable
149        if getattr(e, "test", False):
150            return
151
152    if action:
153        old_action = action
154        action = None
155        renpy.display.behavior.run(old_action)
156
157    now = renpy.display.core.get_time()
158
159    node, state, start_time = execute_node(now, node, state, start_time)
160
161    labels.clear()
162
163    if node is None:
164        renpy.test.testmouse.reset()
165        return
166
167    loc = renpy.exports.get_filename_line()
168
169    if (old_state != state) or (old_loc != loc):
170        last_state_change = now
171
172    old_state = state
173    old_loc = loc
174
175    if (now - last_state_change) > _test.timeout:
176        raise Exception("Testcase stuck at {}:{}.".format(node_loc[0], node_loc[1]))
177
178
179def test_command():
180    """
181    The dialogue command. This updates dialogue.txt, a file giving all the dialogue
182    in the game.
183    """
184
185    ap = renpy.arguments.ArgumentParser(description="Runs a testcase.")
186    ap.add_argument("testcase", help="The name of a testcase to run.", nargs='?', default="default")
187
188    args = ap.parse_args()
189
190    if args.testcase not in testcases:
191        raise Exception("Testcase {} was not found.".format(args.testcase))
192
193    global node
194    node = testcases[args.testcase]
195
196    return True
197
198
199renpy.arguments.register_command("test", test_command)
200