Added a curses based way to see the test suite running. Works only where curses is implemented. Try it out with:
./dotest.py --results-formatter=test_results.Curses --results-file=/dev/stdout llvm-svn: 248072
This commit is contained in:
parent
acf5bdfd88
commit
1827fc25ab
|
@ -44,7 +44,7 @@ import signal
|
|||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import test_results
|
||||
import dotest_channels
|
||||
import dotest_args
|
||||
|
||||
|
@ -75,7 +75,6 @@ total_tests = None
|
|||
test_name_len = None
|
||||
dotest_options = None
|
||||
output_on_success = False
|
||||
|
||||
RESULTS_FORMATTER = None
|
||||
RUNNER_PROCESS_ASYNC_MAP = None
|
||||
RESULTS_LISTENER_CHANNEL = None
|
||||
|
@ -114,20 +113,22 @@ def setup_global_variables(
|
|||
def report_test_failure(name, command, output):
|
||||
global output_lock
|
||||
with output_lock:
|
||||
print >> sys.stderr
|
||||
print >> sys.stderr, output
|
||||
print >> sys.stderr, "[%s FAILED]" % name
|
||||
print >> sys.stderr, "Command invoked: %s" % ' '.join(command)
|
||||
if not (RESULTS_FORMATTER and RESULTS_FORMATTER.is_using_terminal()):
|
||||
print >> sys.stderr
|
||||
print >> sys.stderr, output
|
||||
print >> sys.stderr, "[%s FAILED]" % name
|
||||
print >> sys.stderr, "Command invoked: %s" % ' '.join(command)
|
||||
update_progress(name)
|
||||
|
||||
|
||||
def report_test_pass(name, output):
|
||||
global output_lock, output_on_success
|
||||
with output_lock:
|
||||
if output_on_success:
|
||||
print >> sys.stderr
|
||||
print >> sys.stderr, output
|
||||
print >> sys.stderr, "[%s PASSED]" % name
|
||||
if not (RESULTS_FORMATTER and RESULTS_FORMATTER.is_using_terminal()):
|
||||
if output_on_success:
|
||||
print >> sys.stderr
|
||||
print >> sys.stderr, output
|
||||
print >> sys.stderr, "[%s PASSED]" % name
|
||||
update_progress(name)
|
||||
|
||||
|
||||
|
@ -135,10 +136,11 @@ def update_progress(test_name=""):
|
|||
global output_lock, test_counter, total_tests, test_name_len
|
||||
with output_lock:
|
||||
counter_len = len(str(total_tests))
|
||||
sys.stderr.write(
|
||||
"\r%*d out of %d test suites processed - %-*s" %
|
||||
(counter_len, test_counter.value, total_tests,
|
||||
test_name_len.value, test_name))
|
||||
if not (RESULTS_FORMATTER and RESULTS_FORMATTER.is_using_terminal()):
|
||||
sys.stderr.write(
|
||||
"\r%*d out of %d test suites processed - %-*s" %
|
||||
(counter_len, test_counter.value, total_tests,
|
||||
test_name_len.value, test_name))
|
||||
if len(test_name) > test_name_len.value:
|
||||
test_name_len.value = len(test_name)
|
||||
test_counter.value += 1
|
||||
|
@ -434,11 +436,13 @@ def find_test_files_in_dir_tree(dir_root, found_func):
|
|||
|
||||
def initialize_global_vars_common(num_threads, test_work_items):
|
||||
global total_tests, test_counter, test_name_len
|
||||
|
||||
total_tests = sum([len(item[1]) for item in test_work_items])
|
||||
test_counter = multiprocessing.Value('i', 0)
|
||||
test_name_len = multiprocessing.Value('i', 0)
|
||||
print >> sys.stderr, "Testing: %d test suites, %d thread%s" % (
|
||||
total_tests, num_threads, (num_threads > 1) * "s")
|
||||
if not (RESULTS_FORMATTER and RESULTS_FORMATTER.is_using_terminal()):
|
||||
print >> sys.stderr, "Testing: %d test suites, %d thread%s" % (
|
||||
total_tests, num_threads, (num_threads > 1) * "s")
|
||||
update_progress()
|
||||
|
||||
|
||||
|
@ -1162,9 +1166,10 @@ def main(print_details_on_success, num_threads, test_subdir,
|
|||
|
||||
dotest_argv = sys.argv[1:]
|
||||
|
||||
global output_on_success, RESULTS_FORMATTER
|
||||
global output_on_success, RESULTS_FORMATTER, output_lock
|
||||
output_on_success = print_details_on_success
|
||||
RESULTS_FORMATTER = results_formatter
|
||||
RESULTS_FORMATTER.set_lock(output_lock)
|
||||
|
||||
# We can't use sys.path[0] to determine the script directory
|
||||
# because it doesn't work under a debugger
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import time
|
||||
import curses, curses.panel
|
||||
|
||||
class Point(object):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __str__(self):
|
||||
return "(x=%u, y=%u)" % (self.x, self.y)
|
||||
|
||||
def is_valid_coordinate(self):
|
||||
return self.x >= 0 and self.y >= 0
|
||||
|
||||
class Size(object):
|
||||
def __init__(self, w, h):
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __str__(self):
|
||||
return "(w=%u, h=%u)" % (self.w, self.h)
|
||||
|
||||
class Rect(object):
|
||||
def __init__(self, x=0, y=0, w=0, h=0):
|
||||
self.origin = Point(x, y)
|
||||
self.size = Size(w, h)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __str__(self):
|
||||
return "{ %s, %s }" % (str(self.origin), str(self.size))
|
||||
|
||||
def get_min_x(self):
|
||||
return self.origin.x
|
||||
|
||||
def get_max_x(self):
|
||||
return self.origin.x + self.size.w
|
||||
|
||||
def get_min_y(self):
|
||||
return self.origin.y
|
||||
|
||||
def get_max_y(self):
|
||||
return self.origin.y + self.size.h
|
||||
|
||||
def contains_point(self, pt):
|
||||
if pt.x < self.get_max_x():
|
||||
if pt.y < self.get_max_y():
|
||||
if pt.x >= self.get_min_y():
|
||||
return pt.y >= self.get_min_y()
|
||||
return False
|
||||
|
||||
class Window(object):
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
|
||||
def point_in_window(self, pt):
|
||||
size = self.get_size()
|
||||
return pt.x >= 0 and pt.x < size.w and pt.y >= 0 and pt.y < size.h
|
||||
|
||||
def addstr(self, pt, str):
|
||||
try:
|
||||
self.window.addstr(pt.y, pt.x, str)
|
||||
except:
|
||||
pass
|
||||
|
||||
def addnstr(self, pt, str, n):
|
||||
try:
|
||||
self.window.addnstr(pt.y, pt.x, str, n)
|
||||
except:
|
||||
pass
|
||||
|
||||
def box(self):
|
||||
self.window.box()
|
||||
|
||||
def get_contained_rect(self, top_inset=0, bottom_inset=0, left_inset=0, right_inset=0, height=-1, width=-1):
|
||||
'''Get a rectangle based on the top "height" lines of this window'''
|
||||
rect = self.get_frame()
|
||||
x = rect.origin.x + left_inset
|
||||
y = rect.origin.y + top_inset
|
||||
if height == -1:
|
||||
h = rect.size.h - (top_inset + bottom_inset)
|
||||
else:
|
||||
h = height
|
||||
if width == -1:
|
||||
w = rect.size.w - (left_inset + right_inset)
|
||||
else:
|
||||
w = width
|
||||
return Rect (x = x, y = y, w = w, h = h)
|
||||
|
||||
def erase(self):
|
||||
self.window.erase()
|
||||
|
||||
def get_frame(self):
|
||||
position = self.get_position()
|
||||
size = self.get_size()
|
||||
return Rect(x=position.x, y=position.y, w=size.w, h=size.h)
|
||||
|
||||
def get_position(self):
|
||||
(y, x) = self.window.getbegyx()
|
||||
return Point(x, y)
|
||||
|
||||
def get_size(self):
|
||||
(y, x) = self.window.getmaxyx()
|
||||
return Size(w=x, h=y)
|
||||
|
||||
def refresh(self):
|
||||
curses.panel.update_panels()
|
||||
return self.window.refresh()
|
||||
|
||||
def resize(self, size):
|
||||
return window.resize(size.h, size.w)
|
||||
|
||||
class Panel(Window):
|
||||
def __init__(self, frame):
|
||||
window = curses.newwin(frame.size.h,frame.size.w, frame.origin.y, frame.origin.x)
|
||||
super(Panel, self).__init__(window)
|
||||
self.panel = curses.panel.new_panel(window)
|
||||
|
||||
def top(self):
|
||||
self.panel.top()
|
||||
|
||||
def set_position(self, pt):
|
||||
self.panel.move(pt.y, pt.x)
|
||||
|
||||
def slide_position(self, pt):
|
||||
new_position = self.get_position()
|
||||
new_position.x = new_position.x + pt.x
|
||||
new_position.y = new_position.y + pt.y
|
||||
self.set_position(new_position)
|
||||
|
||||
class BoxedPanel(Panel):
|
||||
def __init__(self, frame, title):
|
||||
super(BoxedPanel, self).__init__(frame)
|
||||
self.title = title
|
||||
self.lines = list()
|
||||
self.first_visible_idx = 0
|
||||
self.update()
|
||||
|
||||
def get_usable_width(self):
|
||||
'''Valid usable width is 0 to (width - 3) since the left and right lines display the box around
|
||||
this frame and we skip a leading space'''
|
||||
w = self.get_size().w
|
||||
if w > 3:
|
||||
return w-3
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_usable_height(self):
|
||||
'''Valid line indexes are 0 to (height - 2) since the top and bottom lines display the box around this frame.'''
|
||||
h = self.get_size().h
|
||||
if h > 2:
|
||||
return h-2
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_point_for_line(self, global_line_idx):
|
||||
'''Returns the point to use when displaying a line whose index is "line_idx"'''
|
||||
line_idx = global_line_idx - self.first_visible_idx
|
||||
num_lines = self.get_usable_height()
|
||||
if line_idx < num_lines:
|
||||
return Point(x=2, y=1+line_idx)
|
||||
else:
|
||||
return Point(x=-1, y=-1) # return an invalid coordinate if the line index isn't valid
|
||||
|
||||
def set_title (self, title, update=True):
|
||||
self.title = title
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def _adjust_first_visible_line(self):
|
||||
num_lines = len(self.lines)
|
||||
max_visible_lines = self.get_usable_height()
|
||||
if (num_lines - self.first_visible_idx) > max_visible_lines:
|
||||
self.first_visible_idx = num_lines - max_visible_lines
|
||||
|
||||
def append_line(self, s, update=True):
|
||||
self.lines.append(s)
|
||||
self._adjust_first_visible_line()
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def set_line(self, line_idx, s, update=True):
|
||||
'''Sets a line "line_idx" within the boxed panel to be "s"'''
|
||||
if line_idx < 0:
|
||||
return
|
||||
while line_idx >= len(self.lines):
|
||||
self.lines.append('')
|
||||
self.lines[line_idx] = s
|
||||
self._adjust_first_visible_line()
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self.erase()
|
||||
self.box()
|
||||
if self.title:
|
||||
self.addstr(Point(x=2, y=0), ' ' + self.title + ' ')
|
||||
max_width = self.get_usable_width()
|
||||
for line_idx in range(self.first_visible_idx, len(self.lines)):
|
||||
pt = self.get_point_for_line(line_idx)
|
||||
if pt.is_valid_coordinate():
|
||||
self.addnstr(pt, self.lines[line_idx], max_width)
|
||||
else:
|
||||
return
|
||||
|
||||
class StatusPanel(Panel):
|
||||
def __init__(self, frame):
|
||||
super(StatusPanel, self).__init__(frame)
|
||||
self.status_items = list()
|
||||
self.status_dicts = dict()
|
||||
self.next_status_x = 1
|
||||
|
||||
def add_status_item(self, name, title, format, width, value, update=True):
|
||||
status_item_dict = { 'name': name,
|
||||
'title' : title,
|
||||
'width' : width,
|
||||
'format' : format,
|
||||
'value' : value,
|
||||
'x' : self.next_status_x }
|
||||
index = len(self.status_items)
|
||||
self.status_items.append(status_item_dict)
|
||||
self.status_dicts[name] = index
|
||||
self.next_status_x += width + 2;
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def increment_status(self, name, update=True):
|
||||
if name in self.status_dicts:
|
||||
status_item_idx = self.status_dicts[name]
|
||||
status_item_dict = self.status_items[status_item_idx]
|
||||
status_item_dict['value'] = status_item_dict['value'] + 1
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def update_status(self, name, value, update=True):
|
||||
if name in self.status_dicts:
|
||||
status_item_idx = self.status_dicts[name]
|
||||
status_item_dict = self.status_items[status_item_idx]
|
||||
status_item_dict['value'] = status_item_dict['format'] % (value)
|
||||
if update:
|
||||
self.update()
|
||||
def update(self):
|
||||
self.erase();
|
||||
for status_item_dict in self.status_items:
|
||||
self.addnstr(Point(x=status_item_dict['x'], y=0), '%s: %s' % (status_item_dict['title'], status_item_dict['value']), status_item_dict['width'])
|
||||
|
||||
stdscr = None
|
||||
|
||||
def intialize_curses():
|
||||
global stdscr
|
||||
stdscr = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
stdscr.keypad(1)
|
||||
try:
|
||||
curses.start_color()
|
||||
except:
|
||||
pass
|
||||
return Window(stdscr)
|
||||
|
||||
def terminate_curses():
|
||||
global stdscr
|
||||
if stdscr:
|
||||
stdscr.keypad(0)
|
||||
curses.echo()
|
||||
curses.nocbreak()
|
||||
curses.endwin()
|
||||
|
|
@ -382,6 +382,8 @@ class ResultsFormatter(object):
|
|||
super(ResultsFormatter, self).__init__()
|
||||
self.out_file = out_file
|
||||
self.options = options
|
||||
self.using_terminal = False
|
||||
self.lock = None # used when coordinating output is needed
|
||||
if not self.out_file:
|
||||
raise Exception("ResultsFormatter created with no file object")
|
||||
self.start_time_by_test = {}
|
||||
|
@ -392,6 +394,9 @@ class ResultsFormatter(object):
|
|||
# entirely consistent from the outside.
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def set_lock(self, lock):
|
||||
self.lock = lock
|
||||
|
||||
def handle_event(self, test_event):
|
||||
"""Handles the test event for collection into the formatter output.
|
||||
|
||||
|
@ -436,6 +441,9 @@ class ResultsFormatter(object):
|
|||
del self.start_time_by_test[test_key]
|
||||
return end_time - start_time
|
||||
|
||||
def is_using_terminal(self):
|
||||
"""Returns True if this results formatter is using the terminal and output should be avoided"""
|
||||
return self.using_terminal
|
||||
|
||||
class XunitFormatter(ResultsFormatter):
|
||||
"""Provides xUnit-style formatted output.
|
||||
|
@ -881,6 +889,114 @@ class RawPickledFormatter(ResultsFormatter):
|
|||
self.out_file.send(
|
||||
"{}#{}".format(len(pickled_message), pickled_message))
|
||||
|
||||
class Curses(ResultsFormatter):
|
||||
"""Receives live results from tests that are running and reports them to the terminal in a curses GUI"""
|
||||
|
||||
def clear_line(self, y):
|
||||
self.out_file.write("\033[%u;0H\033[2K" % (y))
|
||||
self.out_file.flush()
|
||||
|
||||
def print_line(self, y, str):
|
||||
self.out_file.write("\033[%u;0H\033[2K%s" % (y, str))
|
||||
self.out_file.flush()
|
||||
|
||||
def __init__(self, out_file, options):
|
||||
# Initialize the parent
|
||||
super(Curses, self).__init__(out_file, options)
|
||||
self.using_terminal = True
|
||||
self.have_curses = True
|
||||
self.initialize_event = None
|
||||
self.jobs = [None] * 64
|
||||
self.job_tests = [None] * 64
|
||||
try:
|
||||
import lldbcurses
|
||||
self.main_window = lldbcurses.intialize_curses()
|
||||
num_jobs = 8 # TODO: need to dynamically determine this
|
||||
job_frame = self.main_window.get_contained_rect(height=num_jobs+2)
|
||||
fail_frame = self.main_window.get_contained_rect(top_inset=num_jobs+2, bottom_inset=1)
|
||||
status_frame = self.main_window.get_contained_rect(height=1, top_inset=self.main_window.get_size().h-1)
|
||||
self.job_panel = lldbcurses.BoxedPanel(job_frame, "Jobs")
|
||||
self.fail_panel = lldbcurses.BoxedPanel(fail_frame, "Failures")
|
||||
self.status_panel = lldbcurses.StatusPanel(status_frame)
|
||||
self.status_panel.add_status_item(name="success", title="Success (%s)" % self.status_to_short_str('success'), format="%u", width=20, value=0, update=False)
|
||||
self.status_panel.add_status_item(name="failure", title="Failure (%s)" % self.status_to_short_str('failure'), format="%u", width=20, value=0, update=False)
|
||||
self.status_panel.add_status_item(name="error", title="Error (%s)" % self.status_to_short_str('error'), format="%u", width=20, value=0, update=False)
|
||||
self.status_panel.add_status_item(name="skip", title="Skipped (%s)" % self.status_to_short_str('skip'), format="%u", width=20, value=0, update=True)
|
||||
self.status_panel.add_status_item(name="expected_failure", title="Expected Failure (%s)" % self.status_to_short_str('expected_failure'), format="%u", width=30, value=0, update=False)
|
||||
self.status_panel.add_status_item(name="unexpected_success", title="Unexpected Success (%s)" % self.status_to_short_str('unexpected_success'), format="%u", width=30, value=0, update=False)
|
||||
self.main_window.refresh()
|
||||
except:
|
||||
self.have_curses = False
|
||||
lldbcurses.terminate_curses()
|
||||
self.using_terminal = False
|
||||
print "Unexpected error:", sys.exc_info()[0]
|
||||
raise
|
||||
|
||||
|
||||
self.line_dict = dict()
|
||||
self.events_file = open("/tmp/events.txt", "w")
|
||||
# self.formatters = list()
|
||||
# if tee_results_formatter:
|
||||
# self.formatters.append(tee_results_formatter)
|
||||
|
||||
def status_to_short_str(self, status):
|
||||
if status == 'success':
|
||||
return '.'
|
||||
elif status == 'failure':
|
||||
return 'F'
|
||||
elif status == 'unexpected_success':
|
||||
return '?'
|
||||
elif status == 'expected_failure':
|
||||
return 'X'
|
||||
elif status == 'skip':
|
||||
return 'S'
|
||||
elif status == 'error':
|
||||
return 'E'
|
||||
else:
|
||||
return status
|
||||
def handle_event(self, test_event):
|
||||
if self.lock:
|
||||
self.lock.acquire()
|
||||
super(Curses, self).handle_event(test_event)
|
||||
# for formatter in self.formatters:
|
||||
# formatter.process_event(test_event)
|
||||
if self.have_curses:
|
||||
worker_index = -1
|
||||
if 'worker_index' in test_event:
|
||||
worker_index = test_event['worker_index']
|
||||
if 'event' in test_event:
|
||||
print >>self.events_file, str(test_event)
|
||||
event = test_event['event']
|
||||
if event == 'test_start':
|
||||
name = test_event['test_class'] + '.' + test_event['test_name']
|
||||
self.job_tests[worker_index] = test_event
|
||||
if 'pid' in test_event:
|
||||
line = 'pid: ' + str(test_event['pid']) + ' ' + name
|
||||
else:
|
||||
line = name
|
||||
self.job_panel.set_line(worker_index, line)
|
||||
self.main_window.refresh()
|
||||
elif event == 'test_result':
|
||||
status = test_event['status']
|
||||
self.status_panel.increment_status(status)
|
||||
self.job_panel.set_line(worker_index, '')
|
||||
# if status != 'success' and status != 'skip' and status != 'expect_failure':
|
||||
name = test_event['test_class'] + '.' + test_event['test_name']
|
||||
time = test_event['event_time'] - self.job_tests[worker_index]['event_time']
|
||||
self.fail_panel.append_line('%s (%6.2f sec) %s' % (self.status_to_short_str(status), time, name))
|
||||
self.main_window.refresh()
|
||||
self.job_tests[worker_index] = ''
|
||||
elif event == 'job_begin':
|
||||
self.jobs[worker_index] = test_event
|
||||
elif event == 'job_end':
|
||||
self.jobs[worker_index] = ''
|
||||
elif event == 'initialize':
|
||||
self.initialize_event = test_event
|
||||
num_jobs = test_event['worker_count']
|
||||
self.main_window.refresh()
|
||||
|
||||
if self.lock:
|
||||
self.lock.release()
|
||||
|
||||
class DumpFormatter(ResultsFormatter):
|
||||
"""Formats events to the file as their raw python dictionary format."""
|
||||
|
|
Loading…
Reference in New Issue