Code covered by the BSD License  

Highlights from
mlunit_2008a

image thumbnail
from mlunit_2008a by Christopher
A MATLAB unit test framework supporting new classdef files (r2008a)

GuiTestRunner
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

Contact us at files@mathworks.com