classdef GuiTestRunner < TestRunner
% GUITESTRUNNER A graphical user interface for running tests.
%
% This class is part of the mlunit_2008a testing framework.
%
% To launch the interface, simply invoke the constructor
% (GuiTestRunner) with no parameters.
properties
% these are mostly holders for the window component handles
fig
buttonPanel
progressPanel
resultPanel
resultTree
resultTreeRootNode
resultMessage
resultStack
runButton
stopButton
addButton
goToButton
progressAxes
successText
errorText
resultText1
resultText2
statusText
listeners
end
methods
% Constructor - create and launch the interface.
function obj = GuiTestRunner()
obj.open();
end
end
methods
%%%%%%%%%%%%%%%%%%%%
% SETUP & TEARDOWN %
%%%%%%%%%%%%%%%%%%%%
function open(self)
self.cleanup();
self.setup();
set(self.fig, 'Visible', 'on');
end
function cleanup(self)
if ishandle(self.fig)
delete(self.fig);
end
self.clearListeners();
end
function clearListeners(self)
if ~isempty(self.listeners)
for i = 1:length(self.listeners)
delete(self.listeners(i));
end
end
self.listeners = event.proplistener.empty;
end
% Construct and initialize the elements of the GUI.
function setup(self)
set(0, 'Units', 'pixels');
screen = get(0, 'ScreenSize');
figx = screen(3) / 2 - 150;
figy = screen(4) / 2 - 200;
self.fig = figure('Visible', 'off', 'Name', 'MLUnit Test Runner',...
'MenuBar', 'none', 'NumberTitle', 'off', ...
'Position', [figx figy 300 400], ...
'ResizeFcn', @(s,e)(self.resizeFigCB(s,e)), ...
'CloseRequestFcn', @(s,e)(self.closeFigCB(s,e)));
self.buttonPanel = uipanel(self.fig, 'BorderType', 'none', ...
'Units', 'pixels', ...
'ResizeFcn', @(s,e)(self.resizeButtonPanelCB(s,e)));
bpane = self.buttonPanel;
self.progressPanel = uipanel(self.fig, 'BorderType', 'none', ...
'Units', 'pixels', ...
'ResizeFcn', @(s,e)(self.resizeProgressPanelCB(s,e)));
ppane = self.progressPanel;
self.resultPanel = uipanel(self.fig, 'BorderType', 'none', ...
'Units', 'pixels', ...
'ResizeFcn', @(s,e)(self.resizeResultPanelCB(s,e)));
rpane = self.resultPanel;
self.addButton = uicontrol(bpane, 'Style', 'pushbutton', ...
'CData', self.getIcon('tests_add.png'), ...
'TooltipString', 'Add tests', ...
'Callback', @(s,e)(self.addTestsCB(s,e)));
self.runButton = uicontrol(bpane, 'Style', 'pushbutton', ...
'CData', self.getIcon('run.png'), ...
'TooltipString', 'Run tests', 'Enable', 'off', ...
'Callback', @(s,e)(self.runTestsCB(s,e)));
self.stopButton = uicontrol(bpane, 'Style', 'pushbutton', ...
'CData', self.getIcon('stop.png'), ...
'TooltipString', 'Stop tests', 'Enable', 'off', ...
'Callback', @(s,e)(self.stopTestsCB(s,e)));
self.statusText = uicontrol(bpane, 'Style', 'text', ...
'HorizontalAlignment', 'left', 'String', '');
self.progressAxes = axes('XTick', [], 'YTick', [], ...
'Units', 'pixels', 'Box', 'on');
set(self.progressAxes, 'Parent', ppane);
self.successText = uicontrol(ppane, 'Style', 'text', ...
'ForegroundColor', 'green', 'String', '', ...
'HorizontalAlignment', 'left');
self.errorText = uicontrol(ppane, 'Style', 'text', ...
'ForegroundColor', 'red', 'String', '', ...
'HorizontalAlignment', 'right');
self.resultText1 = uicontrol(rpane, 'Style', 'text', ...
'HorizontalAlignment', 'left', 'String', 'Tests:');
self.resultText2 = uicontrol(rpane, 'Style', 'text', ...
'HorizontalAlignment', 'left', 'String', 'Error messages:');
self.resultTreeRootNode = uitreenode(1, '', [], true);
self.resultTree = uitree('Parent', rpane, ...
'ExpandFcn', @(s,e)(self.expandResultNodeCB(s,e)), ...
'SelectionChangeFcn', @(s,e)(self.selectResultNodeCB(s,e)), ...
'Root', self.resultTreeRootNode);
t = get(self.resultTree, 'Tree');
set(t, 'RootVisible', 'off');
self.resultMessage = uicontrol(rpane, 'Style', 'listbox', ...
'BackgroundColor', 'white', ...
'TooltipString', 'Select a line and click Go To Code to view/edit code');
self.goToButton = uicontrol(rpane, 'Style', 'pushbutton', ...
'String', 'Go To Code...', 'Enable', 'off',...
'TooltipString', 'Go to selected function', ...
'Callback', @(s,e)(self.goToCB(s,e)));
end
% initialize gets called whenever the list of tests change. It
% clears any previous results, recreates the result tree pane, and
% clears listeners.
function initialize(self)
self.clearListeners();
% this didn't work quite as hoped :(
% -> self.resultTree.removeAllChildren(self.resultTreeRootNode);
% so instead, we'll do things a bit more aggressively by
% recreating the resultTree every time.
delete(self.resultTree);
delete(self.resultTreeRootNode);
self.resultTreeRootNode = uitreenode(1, '', [], true);
self.resultTree = uitree('Parent', self.resultPanel, ...
'ExpandFcn', @(s,e)(self.expandResultNodeCB(s,e)), ...
'SelectionChangeFcn', @(s,e)(self.selectResultNodeCB(s,e)), ...
'Root', self.resultTreeRootNode);
self.resizeResultPanelCB();
initialize@TestRunner(self);
self.testResult.parent = self;
img = self.getResultTreeNodeImage(self.testResult);
set(self.resultTreeRootNode, 'Value', 1, ...
'Name', self.testResult.getDescription(), ...
'LeafNode', false, 'Icon', img);
self.setupListener(self.testResult, self.resultTreeRootNode);
t = get(self.resultTree, 'Tree');
set(t, 'RootVisible', 'on');
self.resultTree.expand(self.resultTreeRootNode);
set(self.resultMessage, 'String', {});
set(self.goToButton, 'Enable', 'off');
self.updateProgress();
end
%%%%%%%%%%%%%
% CALLBACKS %
%%%%%%%%%%%%%
function addTestsCB(self, src, event)
[fnames, pname] = uigetfile('*.m', 'Add Tests', 'Multiselect', 'on');
if ischar(pname)
pname = pname(1:(length(pname)-1));
% handle any package names in the path
idx = findstr(pname, [filesep '+']);
if ~isempty(idx)
pkg = pname((idx(1)+2):length(pname));
pkg = strrep(pkg, [filesep '+'], '.');
pkg = [pkg '.'];
pname = pname(1:(idx(1)-1));
else
pkg = '';
end
if ~strcmp(pname, pwd)
k = strfind(path, pname);
if isempty(k)
q = sprintf('The selected tests appear to not be in your current path; do you want to add ''%s'' to your path?', pname);
userans = questdlg(q, 'Warning: tests not in path', 'Yes', 'No', 'Yes');
if strcmp('Yes', userans)
addpath(pname);
end
end
end
end
if ~iscell(fnames)
fnames = {fnames};
end
names = {};
for i = 1:length(fnames)
[foo, name] = fileparts(fnames{i});
names{i} = [pkg name];
end
self.test = DynamicTestSuite(names);
self.initialize();
set(self.statusText', 'String', '');
set(self.runButton, 'Enable', 'on');
end
function runTestsCB(self, src, event)
self.initialize(); % reload test info; user may change test classes
set(self.statusText, 'String', 'Running...');
set(self.runButton, 'Enable', 'off');
set(self.stopButton, 'Enable', 'on');
drawnow
self.testResult.runTests();
set(self.runButton, 'Enable', 'on');
set(self.stopButton, 'Enable', 'off');
tc = self.testResult.testCount;
fc = self.testResult.failureCount;
sc = self.testResult.successCount;
runs = fc + sc;
if runs < tc
set(self.statusText, 'String', 'Stopped.');
else
set(self.statusText, 'String', '');
end
end
function stopTestsCB(self, src, event)
set(self.statusText, 'String', 'Stopping...');
drawnow expose
self.testResult.stopTests();
set(self.runButton, 'Enable', 'on');
set(self.stopButton, 'Enable', 'off');
set(self.statusText, 'String', 'Stopped.');
end
function goToCB(self, src, event)
idx = get(self.resultMessage, 'Value');
file = self.resultStack(idx).file;
line = self.resultStack(idx).line;
opentoline(file, line);
end
function nodes = expandResultNodeCB(self, tree, value)
result = self.getTestResultFromNodeVal(value);
names = fields(result.children);
for i = 1:length(names)
child = result.children.(names{i});
if result.isSuite()
desc = child.getDescription();
icon = self.getResultTreeNodeImagePath(child);
leaf = false;
else
desc = names{i};
if isstruct(child)
icon = 'fail.png';
elseif strcmp('S', child)
icon = 'succeed.png';
else
icon = 'bullet.png';
end
leaf = true;
end
if ~isempty(icon)
icon = self.getIconPath(icon);
end
% the value field here stores a hierarchical index that
% tells us how to navigate to the matching TestResult. It
% isn't possible to store the TestResult object itself in
% the value.
nodes(i) = uitreenode([value; i], desc, icon, leaf);
if result.isSuite()
self.setupListener(child, nodes(i));
end
end
end
function selectResultNodeCB(self, src, event)
nn = get(self.resultTree, 'SelectedNodes');
if isempty(nn)
set(self.resultMessage, 'String', {});
set(self.goToButton, 'Enable', 'off');
else
node = nn(1);
val = get(node, 'Value');
result = self.getTestResultFromNodeVal(val);
if isstruct(result)
stacklen = length(result.stack);
msg = cell(stacklen, 1);
msg{1} = result.message;
msg{2} = 'In';
% The idea here was to crop off stack elements for
% functions implemented in the test framework itself. The
% info to do so should be stored in the test results, but
% it got too tricky, so instead I just crop off the test
% runner functions and leave the rest in.
for i = 1:(stacklen-2)
s = sprintf(' %s, line %d', result.stack(i).name, result.stack(i).line);
msg{2+i} = s;
end
set(self.resultMessage, 'Value', 1, 'String', msg);
set(self.goToButton, 'Enable', 'on');
self.resultStack = [result.stack(1); result.stack(1); result.stack];
else
set(self.resultMessage, 'String', {});
set(self.goToButton, 'Enable', 'off');
end
end
end
function resizeFigCB(self, src, event)
figpos = get(self.fig, 'Position');
buttony = figpos(4) - 40;
progressy = buttony - 40;
try
set(self.buttonPanel, 'Position', [0 buttony figpos(3) 40]);
set(self.progressPanel, 'Position', [0 progressy figpos(3) 40]);
set(self.resultPanel, 'Position', [0 0 figpos(3) progressy]);
catch
end
end
function resizeButtonPanelCB(self, src, event)
figpos = get(self.resultPanel, 'Position');
set(self.addButton, 'Position', [12 10 24 24]);
set(self.runButton, 'Position', [56 10 24 24]);
set(self.stopButton, 'Position', [86 10 24 24]);
set(self.statusText, 'Position', [130 10 (figpos(4)-10) 20]);
end
function resizeProgressPanelCB(self, src, event)
figpos = get(self.fig, 'Position');
try
set(self.progressAxes, 'Position', [15 30 figpos(3)-30 10]);
set(self.successText, 'Position', [15 0 figpos(3)/2-15 20]);
set(self.errorText, 'Position', [figpos(3)/2 0 figpos(3)/2-15 20]);
catch
end
end
function resizeResultPanelCB(self, src, event)
figpos = get(self.resultPanel, 'Position');
halfy = figpos(4)/2;
topy = figpos(4)-20;
w = figpos(3)-30;
try
set(self.resultText1, 'Position', [15 topy+2 w 16]);
set(self.resultTree, 'Position', [15 halfy+2 w halfy-22]);
set(self.resultText2, 'Position', [15 halfy-20 w 16]);
set(self.resultMessage, 'Position', [15 39 w halfy-61]);
set(self.goToButton, 'Position', [15 10 w 24]);
catch
end
end
% Called whenever a test result changes status, so we can update
% the icons for the associated tree nodes. Mostly this doesn't do
% much, since the tree isn't expanded when we run :(
function resultUpdateListenerCB(self, result, node, src, event)
img = self.getResultTreeNodeImage(result);
set(node, 'Icon', img);
if ~result.isSuite
% then we also need to update our children, if any
if node.getChildCount > 0
names = fields(result.children);
for i = 1:length(names)
child = result.children.(names{i});
if isstruct(child)
icon = 'fail.png';
elseif strcmp('S',child)
icon = 'success.png';
else
icon = 'bullet.png';
end
img = self.getIconJavaImage(icon);
set(node.getChildAt(i), 'Icon', img);
end
end
end
self.resultTree.repaint;
end
function closeFigCB(self, src, event)
self.cleanup();
% Next bit invalidates handles to the test runner, but also
% fixes "clear classes" issue. This fix came from Kimo Johnson
% (thanks Kimo!)
delete(self);
end
%%%%%%%%%%%%%
% UTILITIES %
%%%%%%%%%%%%%
function incrementFailureCount(self)
self.updateProgress();
end
function incrementSuccessCount(self)
self.updateProgress();
end
function updateProgress(self)
tc = self.testResult.testCount;
fc = self.testResult.failureCount;
sc = self.testResult.successCount;
runs = fc + sc;
if strcmp(self.testResult.status, 'fail')
colr = 'r';
else
colr = 'g';
end
axes(self.progressAxes);
barh(runs, colr);
set(self.progressAxes, 'XLim', [0 tc], ...
'XTick', [], 'YTick', []);
stext = sprintf('Sucesses: %d', sc);
ftext = sprintf('Failures: %d', fc);
set(self.successText, 'String', stext);
set(self.errorText, 'String', ftext);
end
% This little function figures out where the test runner class
% lives, and then constructs a path to the icons directory from it.
function p = getIconPath(self, name)
thisp = fileparts(which(class(self)));
p = [thisp filesep 'icons' filesep name];
end
function data = getIcon(self, name)
bgcolor = get(self.buttonPanel, 'BackgroundColor');
data = imread(self.getIconPath(name), 'BackgroundColor', bgcolor);
end
% Okay, this is kind of a pain. When constructing a uitreenode,
% you pass in the path to the icon. However, when updatng the
% icon, you need the actual java image object.
function obj = getIconJavaImage(self, name)
p = self.getIconPath(name);
f = java.io.File(p);
obj = javax.imageio.ImageIO.read(f);
end
function imgpath = getResultTreeNodeImagePath(self, result)
if result.isSuite
typ = 'suite';
else
typ = 'case';
end
status = result.status;
if strcmp(status, 'init')
status = '';
else
status = ['_' status];
end
imgpath = ['test_' typ status '.png'];
end
function img = getResultTreeNodeImage(self, result)
imgpath = self.getResultTreeNodeImagePath(result);
img = self.getIconJavaImage(imgpath);
end
function setupListener(self, result, node)
cb = @(s,e)(self.resultUpdateListenerCB(result, node, s, e));
lh = addlistener(result, 'status', 'PostSet', cb);
idx = length(self.listeners);
self.listeners(idx + 1) = lh;
end
% This uses the index info stored in the value field of the tree
% node to navigate our test result tree and find the associated
% test result. The index is straightforward - just a vector of
% integers, which tell which child to select at each level of the
% tree.
function result = getTestResultFromNodeVal(self, arr)
result = self.testResult;
arr = arr(2:end);
while ~isempty(arr)
names = fields(result.children);
result = result.children.(names{arr(1)});
arr = arr(2:end);
end
end
function display(self)
disp('a GuiTestRunner')
end
end
end