-
Notifications
You must be signed in to change notification settings - Fork 17
/
test-run.py
executable file
·349 lines (300 loc) · 12.3 KB
/
test-run.py
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#!/usr/bin/env python3
"""Tarantool regression test suite front-end."""
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
# How it works (briefly, simplified)
# ##################################
#
# * Get task groups; each task group correspond to a test suite; each task
# group contains workers generator (factory) and task IDs (test_name +
# conf_name).
# * Put task groups to Dispatcher, which:
# * Create task (input) and result (output) queues for each task group.
# * Create and run specified count of workers on these queues.
# * Wait for results on the result queues and calls registered listeners.
# * If some worker done its work, the Dispatcher will run the new one if
# there are tasks.
# * Listeners received messages from workers and timeouts when no messages
# received. Its:
# * Count results statistics.
# * Multiplex screen's output.
# * Log output to per worker log files.
# * Exit us when some test failed.
# * Exit us when no output received from workers during some time.
# * When all workers reported it's done (or exceptional situation occured) the
# main process kill all processes in the same process group as its own to
# prevent 'orphan' worker or tarantool servers from flooding an OS.
# * Exit status is zero (success) when no errors detected and all requested
# tests passed. Otherwise non-zero.
import multiprocessing
import os
import sys
import time
from lib import Options
from lib import saved_env
from lib.colorer import color_stdout
from lib.colorer import separator
from lib.colorer import test_line
from lib.utils import cpu_count
from lib.utils import find_tags
from lib.utils import shlex_quote
from lib.error import TestRunInitError
from lib.utils import print_tail_n
from lib.utils import PY3
from lib.worker import get_task_groups
from lib.worker import get_reproduce_file
from lib.worker import reproduce_task_groups
from lib.worker import print_greetings
from dispatcher import Dispatcher
from listeners import HangError
EXIT_SUCCESS = 0
EXIT_HANG = 1
EXIT_INTERRUPTED = 2
EXIT_FAILED_TEST = 3
EXIT_NOTDONE_TEST = 4
EXIT_INIT_ERROR = 5
EXIT_UNKNOWN_ERROR = 50
def main_loop_parallel():
color_stdout("Started {0}\n".format(" ".join(sys.argv)), schema='tr_text')
args = Options().args
jobs = args.jobs
if jobs < 1:
# faster result I got was with 2 * cpu_count
jobs = 2 * cpu_count()
if jobs > 0:
color_stdout("Running in parallel with %d workers\n\n" % jobs,
schema='tr_text')
randomize = True
color_stdout("Timeout options:\n", schema='tr_text')
color_stdout('-' * 19, "\n", schema='separator')
color_stdout("SERVER_START_TIMEOUT:" . ljust(26) + "{}\n" .
format(args.server_start_timeout), schema='tr_text')
color_stdout("REPLICATION_SYNC_TIMEOUT:" . ljust(26) + "{}\n" .
format(args.replication_sync_timeout), schema='tr_text')
color_stdout("TEST_TIMEOUT:" . ljust(26) + "{}\n" .
format(args.test_timeout), schema='tr_text')
color_stdout("NO_OUTPUT_TIMEOUT:" . ljust(26) + "{}\n" .
format(args.no_output_timeout), schema='tr_text')
color_stdout("\n", schema='tr_text')
task_groups = get_task_groups()
if Options().args.reproduce:
task_groups = reproduce_task_groups(task_groups)
jobs = 1
randomize = False
dispatcher = Dispatcher(task_groups, jobs, randomize)
dispatcher.start()
print_greetings()
color_stdout('\n')
separator('=')
color_stdout('WORKR ', schema='t_name')
test_line('TEST', 'PARAMS')
color_stdout('RESULT\n', schema='test_pass')
separator('-')
try:
is_force = Options().args.is_force
dispatcher.wait()
dispatcher.wait_processes()
separator('-')
has_failed, has_flaked = dispatcher.statistics.print_statistics()
has_undone = dispatcher.report_undone(
verbose=bool(is_force or not has_failed))
if any([has_failed, has_flaked]):
dispatcher.artifacts.save_artifacts()
if has_failed:
return EXIT_FAILED_TEST
if has_undone:
return EXIT_NOTDONE_TEST
except KeyboardInterrupt:
separator('-')
dispatcher.statistics.print_statistics()
dispatcher.report_undone(verbose=False)
raise
except HangError:
separator('-')
dispatcher.statistics.print_statistics()
dispatcher.report_undone(verbose=False)
return EXIT_HANG
return EXIT_SUCCESS
def main_parallel():
res = EXIT_UNKNOWN_ERROR
try:
res = main_loop_parallel()
except KeyboardInterrupt:
color_stdout('\n[Main process] Caught keyboard interrupt\n',
schema='test_var')
res = EXIT_INTERRUPTED
return res
def main_loop_consistent(failed_test_ids):
# find and prepare all tasks/groups, print information
task_groups = get_task_groups().items()
print_greetings()
for name, task_group in task_groups:
# print information about current test suite
color_stdout('\n')
separator('=')
test_line('TEST', 'PARAMS')
color_stdout("RESULT\n", schema='test_pass')
separator('-')
task_ids = task_group['task_ids']
show_reproduce_content = task_group['show_reproduce_content']
if not task_ids:
continue
worker_id = 1
worker = task_group['gen_worker'](worker_id)
for task_id in task_ids:
# The 'run_task' method returns a tuple of two items:
# (short_status, duration). So taking the first
# item of this tuple for failure check.
short_status = worker.run_task(task_id)[0]
if short_status == 'fail':
reproduce_file_path = \
get_reproduce_file(worker.name)
color_stdout('Reproduce file %s\n' %
reproduce_file_path, schema='error')
if show_reproduce_content:
color_stdout("---\n", schema='separator')
print_tail_n(reproduce_file_path)
color_stdout("...\n", schema='separator')
failed_test_ids.append(task_id)
if not Options().args.is_force:
worker.stop_server(cleanup=False)
return
separator('-')
worker.stop_server(silent=False)
color_stdout()
def main_consistent():
color_stdout("Started {0}\n".format(" ".join(sys.argv)), schema='tr_text')
failed_test_ids = []
try:
main_loop_consistent(failed_test_ids)
except KeyboardInterrupt:
color_stdout('[Main loop] Caught keyboard interrupt\n',
schema='test_var')
except RuntimeError as e:
color_stdout("\nFatal error: %s. Execution aborted.\n" % e,
schema='error')
if Options().args.gdb:
time.sleep(100)
return -1
if failed_test_ids and Options().args.is_force:
color_stdout("\n===== %d tests failed:\n" % len(failed_test_ids),
schema='error')
for test_id in failed_test_ids:
color_stdout("----- %s\n" % str(test_id), schema='info')
return (-1 if failed_test_ids else 0)
def show_tags():
# Collect tests in the same way as when we run them.
collected_tags = set()
for name, task_group in get_task_groups().items():
for task_id in task_group['task_ids']:
test_name, _ = task_id
for tag in find_tags(test_name):
collected_tags.add(tag)
for tag in sorted(collected_tags):
color_stdout(tag + '\n')
def show_env():
""" Print new values of changed environment variables.
The format is suitable for sourcing in a shell.
"""
original_env = saved_env()
for k, v in os.environ.items():
# Don't change PWD.
#
# test-run changes current working directory and set PWD
# environment variable. If we'll just export PWD (and
# don't change a current directory), it will be very
# misleading. Moreover, changing the directory by test-run
# is more like an implementation detail. It would be good
# to get rid from this approach in a future.
if k == 'PWD':
continue
# Don't print unchanged environment variables.
#
# It would be harmless, but if we filter them out, the
# output is nicely short.
if original_env.get(k) == v:
continue
color_stdout('export {}={}\n'.format(shlex_quote(k), shlex_quote(v)))
# test-run doesn't call `del os.environ['FOO']` anywhere, so
# all changed variables are present in `os.environ`. We don't
# need an extra traverse over `original_env` as it would be in
# the general case of comparing two dictionaries.
if __name__ == "__main__":
# In Python 3 start method 'spawn' in multiprocessing module becomes
# default on Mac OS.
#
# The 'spawn' method causes re-execution of some code, which is already
# executed in the main process. At least it is seen on the
# lib/__init__.py code, which removes the 'var' directory. Some other
# code may have side effects too, it requires investigation.
#
# The method also requires object serialization that doesn't work when
# objects use lambdas, whose for example used in class TestSuite
# (lib/test_suite.py).
#
# The latter problem is easy to fix, but the former looks more
# fundamental. So we stick to the 'fork' method now.
if PY3:
multiprocessing.set_start_method('fork')
# test-run assumes that text file streams are UTF-8 (as
# contrary to ASCII) on Python 3. It is necessary to process
# non ASCII symbols in test files, result files and so on.
#
# Default text file stream encoding depends on a system
# locale with exception for the POSIX locale (C locale): in
# this case UTF-8 is used (see PEP-0540). Sadly, this
# behaviour is in effect since Python 3.7.
#
# We want to achieve the same behaviour on lower Python
# versions, at least on 3.6.8, which is provided by CentOS 7
# and CentOS 8.
#
# So we hack the open() builtin.
#
# https://stackoverflow.com/a/53347548/1598057
if PY3 and sys.version_info[0:2] < (3, 7):
std_open = __builtins__.open
def open_as_utf8(*args, **kwargs):
if len(args) >= 2:
mode = args[1]
else:
mode = kwargs.get('mode', '')
if 'b' not in mode:
kwargs.setdefault('encoding', 'utf-8')
return std_open(*args, **kwargs)
__builtins__.open = open_as_utf8
status = 0
if Options().args.show_tags:
show_tags()
exit(status)
if Options().args.show_env:
show_env()
exit(status)
try:
force_parallel = bool(Options().args.reproduce)
if not force_parallel and Options().args.jobs == -1:
status = main_consistent()
else:
status = main_parallel()
except TestRunInitError as e:
color_stdout(str(e), '\n', schema='error')
status = EXIT_INIT_ERROR
exit(status)